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 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,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $: << File.expand_path('../../lib', __FILE__)
4
+
5
+ require 'erforscher'
6
+
7
+ exit(Erforscher::Cli.run(ARGV, $stderr))
@@ -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
@@ -0,0 +1,5 @@
1
+ # encoding: utf-8
2
+
3
+ module Erforscher
4
+ VERSION = '1.0.0.pre0'.freeze
5
+ 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
@@ -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
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ class Resource
4
+ def self.instances
5
+ @instances ||= begin
6
+ path = File.expand_path(%(../../resources/instances.json), __FILE__)
7
+ JSON.parse(File.read(path), symbolize_names: true)
8
+ end
9
+ end
10
+ 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