erforscher 1.0.0.pre0

Sign up to get free protection for your applications and to get access to all the features.
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