erforscher 1.0.0.pre0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +44 -0
- data/bin/erforscher +7 -0
- data/lib/erforscher/cli.rb +53 -0
- data/lib/erforscher/configuration.rb +51 -0
- data/lib/erforscher/explorer.rb +19 -0
- data/lib/erforscher/formatter.rb +33 -0
- data/lib/erforscher/hostsfile.rb +94 -0
- data/lib/erforscher/instances.rb +24 -0
- data/lib/erforscher/runner.rb +50 -0
- data/lib/erforscher/version.rb +5 -0
- data/lib/erforscher.rb +20 -0
- data/spec/acceptance/cli_spec.rb +141 -0
- data/spec/erforscher/configuration_spec.rb +113 -0
- data/spec/erforscher/explorer_spec.rb +61 -0
- data/spec/erforscher/formatter_spec.rb +49 -0
- data/spec/erforscher/hostsfile_spec.rb +137 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/fake_ec2.rb +53 -0
- data/spec/support/resource.rb +10 -0
- metadata +86 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f81b9781cb741624366f9c8dfa9bf16aea4732ad
|
4
|
+
data.tar.gz: dadbf7c5d3ff00598569d819ccb9ec119ca566e5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cd64bb14f561e9407d536c35f1891f12d9f1b23defd37f6286c8b9e14076a4904cf995a84d216dbb644cf19044ff55d59b77c0416dad22d6354d6018d44b3d3a
|
7
|
+
data.tar.gz: 2e5a3a567b4f3352f6ce2e8a61eb69f2111008790c6bb594209588c323c11e9eb77657a0d03d62ac9517553f7efe1392a9aa791b5b3b8ebd1828d0daa7cfafc7
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Erforscher
|
2
|
+
|
3
|
+
Erforscher is a poor man's service discovery tool that uses the AWS EC2 APIs to
|
4
|
+
(optionally) filter instance from a configured set of `tags` and writes
|
5
|
+
hostnames (derived from a configured `name` tag) and private IP address mappings
|
6
|
+
to `/etc/hosts` (or any file of your choosing).
|
7
|
+
|
8
|
+
By default, Erforscher will look for a configuration file in the following
|
9
|
+
places (and in the given order);
|
10
|
+
|
11
|
+
* $HOME/.erforscher.yml
|
12
|
+
* /etc/erforscher.yml
|
13
|
+
|
14
|
+
As indicated below it's also possible to give the path to a configuration file
|
15
|
+
using the `-c|--config` option, and in that case the given path will be used.
|
16
|
+
|
17
|
+
Otherwise, Erforscher will use the first file that exists, and will not attempt
|
18
|
+
to merge settings if there should be files in the above mentioned places.
|
19
|
+
|
20
|
+
## Installation
|
21
|
+
|
22
|
+
No, not currently, but it will be available as a `gem` and as `omnibus`
|
23
|
+
packages, whenever it's ready for releasing out into the wild.
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
```bash
|
28
|
+
erforscher --help
|
29
|
+
erforscher -T|--tags <K1=V1,K2=V2>
|
30
|
+
erforscher -N|--name <TAG>
|
31
|
+
erforscher -c|--config <PATH>
|
32
|
+
```
|
33
|
+
|
34
|
+
The configuration file is a simple YAML file, with the following structure:
|
35
|
+
|
36
|
+
```yaml
|
37
|
+
name_tag: 'Name'
|
38
|
+
region: 'us-east-1'
|
39
|
+
tags:
|
40
|
+
- environment: 'test'
|
41
|
+
service: 'web'
|
42
|
+
- environment: 'production'
|
43
|
+
service: 'database'
|
44
|
+
```
|
data/bin/erforscher
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
class Cli
|
5
|
+
def self.run(argv, io)
|
6
|
+
new.run(argv, io)
|
7
|
+
end
|
8
|
+
|
9
|
+
def run(argv, io)
|
10
|
+
config = parse_options(argv)
|
11
|
+
if config['print_usage']
|
12
|
+
io.puts(@parser)
|
13
|
+
2
|
14
|
+
else
|
15
|
+
Runner.run(config)
|
16
|
+
end
|
17
|
+
rescue => e
|
18
|
+
io.puts(%(#{e.message} (#{e.class.name})))
|
19
|
+
io.puts(@parser)
|
20
|
+
1
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def parse_options(argv, cli={})
|
26
|
+
@parser = OptionParser.new do |parser|
|
27
|
+
parser.on('-c', '--config=PATH', 'Path to configuration file') do |config_path|
|
28
|
+
cli['config_path'] = config_path
|
29
|
+
end
|
30
|
+
|
31
|
+
parser.on('-T', '--tags=LIST', Array, 'List of `tag` key-value pairs') do |tags|
|
32
|
+
cli['tags'] = [parse_tags(tags)]
|
33
|
+
end
|
34
|
+
|
35
|
+
parser.on('-N', '--name=TAG', '"Name" `tag`') do |name_tag|
|
36
|
+
cli['name_tag'] = name_tag
|
37
|
+
end
|
38
|
+
|
39
|
+
parser.on('-h', '--help', 'You\'re looking at it') do
|
40
|
+
cli['print_usage'] = true
|
41
|
+
end
|
42
|
+
end
|
43
|
+
@parser.parse(argv)
|
44
|
+
Configuration.new(cli).get
|
45
|
+
end
|
46
|
+
|
47
|
+
def parse_tags(keys_values)
|
48
|
+
if keys_values.any? { |kv| kv.include?('=') }
|
49
|
+
Hash[keys_values.map { |kv| kv.split('=') }]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
ConfigurationError = Class.new(ErforscherError)
|
5
|
+
|
6
|
+
class Configuration
|
7
|
+
def initialize(cli, file=File)
|
8
|
+
@cli = cli
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
|
12
|
+
def get
|
13
|
+
if (path = @cli.delete('config_path'))
|
14
|
+
unless @file.exist?(path)
|
15
|
+
raise ConfigurationError, 'Could not find any configuration file'
|
16
|
+
end
|
17
|
+
config = load_config(path)
|
18
|
+
elsif @file.exist?(home_path)
|
19
|
+
config = load_config(home_path)
|
20
|
+
elsif @file.exist?(etc_path)
|
21
|
+
config = load_config(etc_path)
|
22
|
+
else
|
23
|
+
config = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
defaults.merge(config).merge(@cli)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def load_config(path)
|
32
|
+
YAML.load(@file.read(path))
|
33
|
+
end
|
34
|
+
|
35
|
+
def home_path
|
36
|
+
File.join(ENV['HOME'], '.erforscher.yml')
|
37
|
+
end
|
38
|
+
|
39
|
+
def etc_path
|
40
|
+
'/etc/erforscher.yml'
|
41
|
+
end
|
42
|
+
|
43
|
+
def defaults
|
44
|
+
{
|
45
|
+
'name_tag' => 'Name',
|
46
|
+
'hostsfile_path' => '/etc/hosts',
|
47
|
+
'tags' => []
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
class Explorer
|
5
|
+
def initialize(instances)
|
6
|
+
@instances = instances
|
7
|
+
end
|
8
|
+
|
9
|
+
def discover(tag_combos)
|
10
|
+
if tag_combos.empty?
|
11
|
+
@instances.all
|
12
|
+
else
|
13
|
+
tag_combos.flat_map do |tags|
|
14
|
+
@instances.with_tags(tags)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
class Formatter
|
5
|
+
def initialize(name_tag, fallback_to_private_dns=true)
|
6
|
+
@name_tag = name_tag
|
7
|
+
@fallback_to_private_dns = fallback_to_private_dns
|
8
|
+
end
|
9
|
+
|
10
|
+
def format(instance)
|
11
|
+
[hostname(instance), instance.private_ip_address].join("\t")
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def hostname(instance)
|
17
|
+
if (name = find_name(instance.tags))
|
18
|
+
strip_domain(name)
|
19
|
+
elsif @fallback_to_private_dns
|
20
|
+
instance.private_dns_name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_name(tags)
|
25
|
+
tag = tags.find { |tag| tag[:key] == @name_tag }
|
26
|
+
tag.value if tag
|
27
|
+
end
|
28
|
+
|
29
|
+
def strip_domain(name)
|
30
|
+
name.split('.').first
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
class Hostsfile
|
5
|
+
def initialize(current_file, new_file, options={})
|
6
|
+
@current_file = current_file
|
7
|
+
@new_file = new_file
|
8
|
+
@fileutils = options[:fileutils] || FileUtils
|
9
|
+
@previously_written = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def write(entries)
|
13
|
+
copy_until_header
|
14
|
+
write_header
|
15
|
+
write_body(entries)
|
16
|
+
write_footer
|
17
|
+
discard_old_entries
|
18
|
+
copy_after_footer
|
19
|
+
end
|
20
|
+
|
21
|
+
def switchero
|
22
|
+
@new_file.close
|
23
|
+
@fileutils.mv(current_path, current_prev_path)
|
24
|
+
@fileutils.mv(new_path, current_path)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def new_path
|
30
|
+
@new_file.path
|
31
|
+
end
|
32
|
+
|
33
|
+
def current_path
|
34
|
+
absolute_path(@current_file)
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_prev_path
|
38
|
+
%(#{absolute_path(@current_file)}.prev)
|
39
|
+
end
|
40
|
+
|
41
|
+
def absolute_path(f)
|
42
|
+
File.absolute_path(f)
|
43
|
+
end
|
44
|
+
|
45
|
+
def header
|
46
|
+
'# ERFORSCHER START'
|
47
|
+
end
|
48
|
+
|
49
|
+
def footer
|
50
|
+
'# ERFORSCHER END'
|
51
|
+
end
|
52
|
+
|
53
|
+
def copy_until_header
|
54
|
+
while (line = @current_file.gets)
|
55
|
+
if line.match(header)
|
56
|
+
@previously_written = true
|
57
|
+
break
|
58
|
+
else
|
59
|
+
@new_file.write(line)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def write_header
|
65
|
+
@new_file.puts(header)
|
66
|
+
end
|
67
|
+
|
68
|
+
def write_body(entries)
|
69
|
+
entries.each do |entry|
|
70
|
+
@new_file.puts(entry)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def write_footer
|
75
|
+
@new_file.puts(footer)
|
76
|
+
end
|
77
|
+
|
78
|
+
def discard_old_entries
|
79
|
+
if @previously_written
|
80
|
+
while (line = @current_file.gets)
|
81
|
+
if line.match(footer)
|
82
|
+
break
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def copy_after_footer
|
89
|
+
while (line = @current_file.gets)
|
90
|
+
@new_file.write(line)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
class Instances
|
5
|
+
def initialize(ec2)
|
6
|
+
@ec2 = ec2
|
7
|
+
end
|
8
|
+
|
9
|
+
def all
|
10
|
+
instances_from(@ec2.describe_instances)
|
11
|
+
end
|
12
|
+
|
13
|
+
def with_tags(tags)
|
14
|
+
filters = tags.map { |key, value| {name: %(tag:#{key}), values: [value]} }
|
15
|
+
instances_from(@ec2.describe_instances(filters: filters))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def instances_from(response)
|
21
|
+
response.flat_map(&:reservations).flat_map(&:instances)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Erforscher
|
4
|
+
class Runner
|
5
|
+
def initialize(config)
|
6
|
+
@config = config
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.run(config)
|
10
|
+
new(config).run
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
explored = explorer.discover(@config['tags'])
|
15
|
+
entries = explored.sort.map { |ex| formatter.format(ex) }
|
16
|
+
hostsfile.write(entries)
|
17
|
+
hostsfile.switchero
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def formatter
|
23
|
+
@formatter ||= Formatter.new(@config['name_tag'])
|
24
|
+
end
|
25
|
+
|
26
|
+
def current_hostsfile
|
27
|
+
@current_hostsfile ||= File.open(@config['hostsfile_path'])
|
28
|
+
end
|
29
|
+
|
30
|
+
def working_file
|
31
|
+
@working_file ||= Tempfile.new(%(erforscher-#{Time.now.to_i}))
|
32
|
+
end
|
33
|
+
|
34
|
+
def hostsfile
|
35
|
+
@hostsfile ||= Hostsfile.new(current_hostsfile, working_file)
|
36
|
+
end
|
37
|
+
|
38
|
+
def explorer
|
39
|
+
@explorer ||= Explorer.new(instances)
|
40
|
+
end
|
41
|
+
|
42
|
+
def instances
|
43
|
+
@instances ||= Instances.new(ec2)
|
44
|
+
end
|
45
|
+
|
46
|
+
def ec2
|
47
|
+
@ec2 ||= Aws::EC2.new(region: @config['region'])
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/erforscher.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'aws-sdk-core'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'yaml'
|
7
|
+
require 'optparse'
|
8
|
+
|
9
|
+
|
10
|
+
module Erforscher
|
11
|
+
ErforscherError = Class.new(StandardError)
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'erforscher/cli'
|
15
|
+
require 'erforscher/configuration'
|
16
|
+
require 'erforscher/explorer'
|
17
|
+
require 'erforscher/formatter'
|
18
|
+
require 'erforscher/hostsfile'
|
19
|
+
require 'erforscher/instances'
|
20
|
+
require 'erforscher/runner'
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe 'bin/erforscher' do
|
6
|
+
let :io do
|
7
|
+
StringIO.new
|
8
|
+
end
|
9
|
+
|
10
|
+
let :ec2 do
|
11
|
+
FakeEc2.new(Resource.instances)
|
12
|
+
end
|
13
|
+
|
14
|
+
let :config do
|
15
|
+
{'hostsfile_path' => hostsfile.path}
|
16
|
+
end
|
17
|
+
|
18
|
+
let :hostsfile do
|
19
|
+
Tempfile.new('etc-hosts')
|
20
|
+
end
|
21
|
+
|
22
|
+
let :config_file do
|
23
|
+
Tempfile.new('config.yml')
|
24
|
+
end
|
25
|
+
|
26
|
+
before do
|
27
|
+
Aws::EC2.stub(:new).and_return(ec2)
|
28
|
+
end
|
29
|
+
|
30
|
+
let :new_hostsfile do
|
31
|
+
File.read(config['hostsfile_path'])
|
32
|
+
end
|
33
|
+
|
34
|
+
let :argv do
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
|
38
|
+
let :run_command do
|
39
|
+
Erforscher::Cli.run(%W[--config #{config_file.path}] + argv, io)
|
40
|
+
end
|
41
|
+
|
42
|
+
let :code do
|
43
|
+
run_command
|
44
|
+
end
|
45
|
+
|
46
|
+
before do
|
47
|
+
config_file.puts(YAML.dump(config))
|
48
|
+
config_file.rewind
|
49
|
+
end
|
50
|
+
|
51
|
+
after do
|
52
|
+
hostsfile.delete
|
53
|
+
config_file.delete
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when given --help or -h' do
|
57
|
+
it 'returns 2 and prints usage' do
|
58
|
+
code = Erforscher::Cli.run(%w[-h], io)
|
59
|
+
expect(code).to eq(2)
|
60
|
+
expect(io.string).to include 'Usage:'
|
61
|
+
|
62
|
+
io.rewind
|
63
|
+
|
64
|
+
code = Erforscher::Cli.run(%w[--help], io)
|
65
|
+
expect(code).to eq(2)
|
66
|
+
expect(io.string).to include 'Usage:'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'when given --config' do
|
71
|
+
context 'and configuration file exists' do
|
72
|
+
let :config do
|
73
|
+
{
|
74
|
+
'hostsfile_path' => hostsfile.path,
|
75
|
+
'tags' => [{
|
76
|
+
'Environment' => 'test',
|
77
|
+
'Created-By' => 'no-one'
|
78
|
+
}]
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
before do
|
83
|
+
run_command
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'returns 0' do
|
87
|
+
expect(code).to eq(0)
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'prints nothing' do
|
91
|
+
expect(io.string).to be_empty
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'uses configuration from given path' do
|
95
|
+
expect(new_hostsfile).to include("instance-001\t10.0.0.1")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'and configuration file does not exist' do
|
100
|
+
it 'returns 1' do
|
101
|
+
code = Erforscher::Cli.run(%w[--config non-existing-file.yml], io)
|
102
|
+
expect(code).to eq(1)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'prints an error message and usage' do
|
106
|
+
Erforscher::Cli.run(%w[--config non-existing-file.yml], io)
|
107
|
+
|
108
|
+
expect(io.string).to include('Could not find any configuration file')
|
109
|
+
expect(io.string).to include('Usage:')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'when given -T or --tags' do
|
115
|
+
let :argv do
|
116
|
+
%w[-T Environment=Cli]
|
117
|
+
end
|
118
|
+
|
119
|
+
before do
|
120
|
+
run_command
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'filters using given tags' do
|
124
|
+
expect(new_hostsfile).to include("instance-003\t10.0.0.3")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'when given -N or --name' do
|
129
|
+
let :argv do
|
130
|
+
%w[-T Environment=other -N NameTag]
|
131
|
+
end
|
132
|
+
|
133
|
+
before do
|
134
|
+
run_command
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'uses given `tag` for parsing hostnames' do
|
138
|
+
expect(new_hostsfile).to include("instance-004\t10.0.0.4")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Erforscher
|
6
|
+
describe Configuration do
|
7
|
+
let :configuration do
|
8
|
+
described_class.new(cli_options, file)
|
9
|
+
end
|
10
|
+
|
11
|
+
let :file do
|
12
|
+
double(:file)
|
13
|
+
end
|
14
|
+
|
15
|
+
let :cli_options do
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
|
19
|
+
before do
|
20
|
+
@home = ENV['HOME']
|
21
|
+
ENV['HOME'] = '/fake/home'
|
22
|
+
end
|
23
|
+
|
24
|
+
before do
|
25
|
+
file.stub(:exist?)
|
26
|
+
end
|
27
|
+
|
28
|
+
after do
|
29
|
+
ENV['HOME'] = @home
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#get' do
|
33
|
+
context 'when given path to a configuration file' do
|
34
|
+
context 'that exist' do
|
35
|
+
let :config do
|
36
|
+
{'name_tag' => 'ConfigFileName'}
|
37
|
+
end
|
38
|
+
|
39
|
+
let :cli_options do
|
40
|
+
{'config_path' => 'this-is-a-config.yml'}
|
41
|
+
end
|
42
|
+
|
43
|
+
before do
|
44
|
+
file.stub(:exist?).with('this-is-a-config.yml').and_return(true)
|
45
|
+
file.stub(:read).with('this-is-a-config.yml').and_return(YAML.dump(config))
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'uses configuration from path' do
|
49
|
+
conf = configuration.get
|
50
|
+
expect(conf['name_tag']).to eq('ConfigFileName')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'that does not exist' do
|
55
|
+
let :cli_options do
|
56
|
+
{'config_path' => 'this-should-not-exist.yml'}
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'raises an error' do
|
60
|
+
expect { configuration.get }.to raise_error
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'when not given path to a configuration file' do
|
66
|
+
context 'and $HOME/.erforscher.yml exist' do
|
67
|
+
let :config do
|
68
|
+
{'name_tag' => 'HomeName'}
|
69
|
+
end
|
70
|
+
|
71
|
+
before do
|
72
|
+
file.stub(:exist?).with('/fake/home/.erforscher.yml').and_return(true)
|
73
|
+
file.stub(:read).with('/fake/home/.erforscher.yml').and_return(YAML.dump(config))
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'uses it' do
|
77
|
+
conf = configuration.get
|
78
|
+
expect(conf['name_tag']).to eq('HomeName')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'and $HOME/.erforscher.yml does not exist' do
|
83
|
+
context 'and /etc/erforscher.yml exist' do
|
84
|
+
let :config do
|
85
|
+
{'name_tag' => 'EtcName'}
|
86
|
+
end
|
87
|
+
|
88
|
+
before do
|
89
|
+
file.stub(:exist?).with('/etc/erforscher.yml').and_return(true)
|
90
|
+
file.stub(:read).with('/etc/erforscher.yml').and_return(YAML.dump(config))
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'uses it' do
|
94
|
+
conf = configuration.get
|
95
|
+
expect(conf['name_tag']).to eq('EtcName')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'and /etc/erforscher.yml does not exist' do
|
100
|
+
it 'uses default configuration' do
|
101
|
+
conf = configuration.get
|
102
|
+
expect(conf).to eq({
|
103
|
+
'name_tag' => 'Name',
|
104
|
+
'hostsfile_path' => '/etc/hosts',
|
105
|
+
'tags' => []
|
106
|
+
})
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Erforscher
|
6
|
+
describe Explorer do
|
7
|
+
let :explorer do
|
8
|
+
described_class.new(instances)
|
9
|
+
end
|
10
|
+
|
11
|
+
let :instances do
|
12
|
+
Instances.new(FakeEc2.new(Resource.instances))
|
13
|
+
end
|
14
|
+
|
15
|
+
let :filtered do
|
16
|
+
explorer.discover(tags)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#discover' do
|
20
|
+
context 'with a single tag' do
|
21
|
+
let :tags do
|
22
|
+
[{'Environment' => 'test'}]
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'filters instances by given tag' do
|
26
|
+
expect(filtered).to have(2).items
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with multiple tags' do
|
31
|
+
let :tags do
|
32
|
+
[{'Environment' => 'test', 'Group' => 'Some group'}]
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'filters instances by given tags' do
|
36
|
+
expect(filtered).to have(1).item
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with multiple tag combinations' do
|
41
|
+
let :tags do
|
42
|
+
[{'Environment' => 'test', 'Group' => 'Some group'}, {'Environment' => 'test2'}]
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'concats filtered instances' do
|
46
|
+
expect(filtered).to have(2).items
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'when given an empty list of tag combinations' do
|
51
|
+
let :tags do
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns all instances' do
|
56
|
+
expect(filtered).to have(5).items
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Erforscher
|
6
|
+
describe Formatter do
|
7
|
+
let :formatter do
|
8
|
+
described_class.new(name_tag)
|
9
|
+
end
|
10
|
+
|
11
|
+
let :name_tag do
|
12
|
+
'Name'
|
13
|
+
end
|
14
|
+
|
15
|
+
let :instance do
|
16
|
+
OpenStruct.new({
|
17
|
+
tags: [
|
18
|
+
OpenStruct.new({key: 'Name', value: 'hello-world-001.domain.com'})
|
19
|
+
],
|
20
|
+
private_ip_address: '10.0.0.1',
|
21
|
+
private_dns_name: 'private-dns-name',
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
let :formatted do
|
26
|
+
formatter.format(instance)
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when instance does have a \'name tag\'' do
|
30
|
+
it 'parses hostname from name tag' do
|
31
|
+
expect(formatted).to include('hello-world-001')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns a tab separated string with hostname and IP address' do
|
35
|
+
expect(formatted).to eq("hello-world-001\t10.0.0.1")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when instance does not have a \'name tag\'' do
|
40
|
+
let :name_tag do
|
41
|
+
'name'
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'uses private DNS name instead' do
|
45
|
+
expect(formatted).to eq("private-dns-name\t10.0.0.1")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
module Erforscher
|
6
|
+
describe Hostsfile do
|
7
|
+
let :hostsfile do
|
8
|
+
described_class.new(current_file, new_file, fileutils: fileutils)
|
9
|
+
end
|
10
|
+
|
11
|
+
let :new_file do
|
12
|
+
StringIO.new
|
13
|
+
end
|
14
|
+
|
15
|
+
let :fileutils do
|
16
|
+
double(:fileutils).as_null_object
|
17
|
+
end
|
18
|
+
|
19
|
+
let :entries do
|
20
|
+
3.times.map { |i| %(host-00#{i}\t10.0.0.#{i}) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def next_line
|
24
|
+
(line = new_file.gets) && line.strip
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#write' do
|
28
|
+
before do
|
29
|
+
hostsfile.write(entries)
|
30
|
+
new_file.rewind
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'a pristine file' do
|
34
|
+
let :current_file do
|
35
|
+
StringIO.new.tap do |io|
|
36
|
+
io.puts %(# Comment at the start of file)
|
37
|
+
io.puts %(127.0.0.1\tno-place-like.home)
|
38
|
+
io.rewind
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'copies everything until header to new file' do
|
43
|
+
expect(next_line).to eq(%(# Comment at the start of file))
|
44
|
+
expect(next_line).to eq(%(127.0.0.1\tno-place-like.home))
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'writes entries to new file' do
|
48
|
+
3.times { next_line }
|
49
|
+
expect(next_line).to eq("host-000\t10.0.0.0")
|
50
|
+
expect(next_line).to eq("host-001\t10.0.0.1")
|
51
|
+
expect(next_line).to eq("host-002\t10.0.0.2")
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'writes a footer' do
|
55
|
+
6.times { next_line }
|
56
|
+
expect(next_line).to eq('# ERFORSCHER END')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'copies everything after footer to new file' do
|
60
|
+
7.times { next_line }
|
61
|
+
expect(next_line).to be_nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'a file that has been written before' do
|
66
|
+
let :current_file do
|
67
|
+
StringIO.new.tap do |io|
|
68
|
+
io.puts %(# Comment at the start of file)
|
69
|
+
io.puts %(127.0.0.1\tno-place-like.home)
|
70
|
+
io.puts %(# ERFORSCHER START)
|
71
|
+
io.puts %(host-000\t10.0.0.0)
|
72
|
+
io.puts %(# ERFORSCHER END)
|
73
|
+
io.puts %(# Something after footer)
|
74
|
+
io.rewind
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'copies everything until header to new file' do
|
79
|
+
expect(next_line).to eq(%(# Comment at the start of file))
|
80
|
+
expect(next_line).to eq(%(127.0.0.1\tno-place-like.home))
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'writes entries to new file' do
|
84
|
+
3.times { next_line }
|
85
|
+
expect(next_line).to eq("host-000\t10.0.0.0")
|
86
|
+
expect(next_line).to eq("host-001\t10.0.0.1")
|
87
|
+
expect(next_line).to eq("host-002\t10.0.0.2")
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'writes footer once' do
|
91
|
+
6.times { next_line }
|
92
|
+
expect(next_line).to eq('# ERFORSCHER END')
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'does not keep old entries around' do
|
96
|
+
7.times { next_line }
|
97
|
+
expect(next_line).to eq('# Something after footer')
|
98
|
+
expect(next_line).to be_nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe '#switchero' do
|
104
|
+
let :new_file do
|
105
|
+
Tempfile.new('new_file')
|
106
|
+
end
|
107
|
+
|
108
|
+
let :current_file do
|
109
|
+
Tempfile.new('current_file')
|
110
|
+
end
|
111
|
+
|
112
|
+
let :absolute_current_path do
|
113
|
+
File.absolute_path(current_file)
|
114
|
+
end
|
115
|
+
|
116
|
+
let :absolute_new_path do
|
117
|
+
File.absolute_path(new_file)
|
118
|
+
end
|
119
|
+
|
120
|
+
before do
|
121
|
+
hostsfile.switchero
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'closes the new file' do
|
125
|
+
new_file.should be_closed
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'backups the previous file' do
|
129
|
+
fileutils.should have_received(:mv).with(absolute_current_path, %(#{absolute_current_path}.prev))
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'renames the new file' do
|
133
|
+
fileutils.should have_received(:mv).with(absolute_new_path, absolute_current_path)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
require 'support/fake_ec2'
|
7
|
+
require 'support/resource'
|
8
|
+
|
9
|
+
require 'simplecov'
|
10
|
+
|
11
|
+
SimpleCov.start do
|
12
|
+
add_group 'Source', 'lib'
|
13
|
+
add_group 'Unit tests', 'spec/erforscher'
|
14
|
+
add_group 'Acceptance tests', 'spec/acceptance'
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'erforscher'
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class FakeEc2
|
4
|
+
def initialize(instances)
|
5
|
+
@instances = instances.map do |instance|
|
6
|
+
tags = instance.delete(:tags)
|
7
|
+
tags = tags.map { |tag| OpenStruct.new(tag) }
|
8
|
+
instance[:tags] = tags
|
9
|
+
OpenStruct.new(instance)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def describe_instances(opts={})
|
14
|
+
filters = opts.fetch(:filters, []).dup
|
15
|
+
instances = @instances.dup
|
16
|
+
|
17
|
+
while filters.any? do
|
18
|
+
filter = filters.shift
|
19
|
+
key = filter[:name].split(':').last
|
20
|
+
value = filter[:values].first
|
21
|
+
|
22
|
+
instances = instances.select do |instance|
|
23
|
+
instance.tags.any? do |tag|
|
24
|
+
tag[:key] == key && tag[:value] == value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
[FakeResponse.new(instances)]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
class FakeResponse
|
35
|
+
def initialize(instances)
|
36
|
+
@instances = instances
|
37
|
+
end
|
38
|
+
|
39
|
+
def reservations
|
40
|
+
@instances.map { |instance| FakeReservation.new(instance) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class FakeReservation
|
45
|
+
def initialize(instance)
|
46
|
+
@instance = instance
|
47
|
+
end
|
48
|
+
|
49
|
+
def instances
|
50
|
+
[@instance]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: erforscher
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0.pre0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mathias Söderberg
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: aws-sdk-core
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Poor man's service discovery tool using AWS EC2 APIs
|
28
|
+
email:
|
29
|
+
- mths@sdrbrg.se
|
30
|
+
executables:
|
31
|
+
- erforscher
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- README.md
|
36
|
+
- bin/erforscher
|
37
|
+
- lib/erforscher.rb
|
38
|
+
- lib/erforscher/cli.rb
|
39
|
+
- lib/erforscher/configuration.rb
|
40
|
+
- lib/erforscher/explorer.rb
|
41
|
+
- lib/erforscher/formatter.rb
|
42
|
+
- lib/erforscher/hostsfile.rb
|
43
|
+
- lib/erforscher/instances.rb
|
44
|
+
- lib/erforscher/runner.rb
|
45
|
+
- lib/erforscher/version.rb
|
46
|
+
- spec/acceptance/cli_spec.rb
|
47
|
+
- spec/erforscher/configuration_spec.rb
|
48
|
+
- spec/erforscher/explorer_spec.rb
|
49
|
+
- spec/erforscher/formatter_spec.rb
|
50
|
+
- spec/erforscher/hostsfile_spec.rb
|
51
|
+
- spec/spec_helper.rb
|
52
|
+
- spec/support/fake_ec2.rb
|
53
|
+
- spec/support/resource.rb
|
54
|
+
homepage: https://github.com/mthssdrbrg/erforscher
|
55
|
+
licenses:
|
56
|
+
- Apache License 2.0
|
57
|
+
metadata: {}
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.0'
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
requirements:
|
69
|
+
- - ">"
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 1.3.1
|
72
|
+
requirements: []
|
73
|
+
rubyforge_project:
|
74
|
+
rubygems_version: 2.2.2
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: AWS EC2 API service discovery tool
|
78
|
+
test_files:
|
79
|
+
- spec/acceptance/cli_spec.rb
|
80
|
+
- spec/erforscher/configuration_spec.rb
|
81
|
+
- spec/erforscher/explorer_spec.rb
|
82
|
+
- spec/erforscher/formatter_spec.rb
|
83
|
+
- spec/erforscher/hostsfile_spec.rb
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
- spec/support/fake_ec2.rb
|
86
|
+
- spec/support/resource.rb
|