elasticsearch-embedded 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Changelog.md +3 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +22 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/bin/embedded-elasticsearch +66 -0
- data/elasticsearch-embedded.gemspec +31 -0
- data/lib/elasticsearch-embedded.rb +1 -0
- data/lib/elasticsearch/embedded.rb +15 -0
- data/lib/elasticsearch/embedded/cluster.rb +303 -0
- data/lib/elasticsearch/embedded/downloader.rb +104 -0
- data/lib/elasticsearch/embedded/logger_configuration.rb +62 -0
- data/lib/elasticsearch/embedded/rspec_configuration.rb +70 -0
- data/lib/elasticsearch/embedded/version.rb +5 -0
- data/spec/lib/elasticsearch/embedded/cluster_spec.rb +93 -0
- data/spec/lib/elasticsearch/embedded/downloader_spec.rb +131 -0
- data/spec/lib/elasticsearch/embedded/rspec_configuration_spec.rb +51 -0
- data/spec/lib/elasticsearch/embedded_spec.rb +39 -0
- data/spec/spec_helper.rb +66 -0
- metadata +184 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'ruby-progressbar'
|
5
|
+
require 'zip'
|
6
|
+
|
7
|
+
module Elasticsearch
|
8
|
+
module Embedded
|
9
|
+
class Downloader
|
10
|
+
|
11
|
+
# Default temporary path used by downloader
|
12
|
+
TEMPORARY_PATH = defined?(Rails) ? Rails.root.join('tmp') : Dir.tmpdir
|
13
|
+
# Default version of elasticsearch to download
|
14
|
+
DEFAULT_VERSION = '1.2.1'
|
15
|
+
|
16
|
+
attr_reader :version, :path
|
17
|
+
|
18
|
+
def initialize(args = {})
|
19
|
+
@version = args[:version] || ENV['ELASTICSEARCH_VERSION'] || DEFAULT_VERSION
|
20
|
+
@path = args[:path] || ENV['ELASTICSEARCH_DOWNLOAD_PATH'] || TEMPORARY_PATH
|
21
|
+
end
|
22
|
+
|
23
|
+
# Download elasticsearch distribution and unzip it in the specified temporary path
|
24
|
+
def self.download(arguments = {})
|
25
|
+
new(arguments).perform
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform
|
29
|
+
download_file
|
30
|
+
extract_file
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
def downloaded?
|
35
|
+
File.exists?(final_path)
|
36
|
+
end
|
37
|
+
|
38
|
+
def extracted?
|
39
|
+
File.directory?(dist_folder)
|
40
|
+
end
|
41
|
+
|
42
|
+
def dist_folder
|
43
|
+
@dist_folder ||= final_path.gsub /\.zip\Z/, ''
|
44
|
+
end
|
45
|
+
|
46
|
+
def final_path
|
47
|
+
@final_path ||= File.join(working_dir, "elasticsearch-#{version}.zip")
|
48
|
+
end
|
49
|
+
|
50
|
+
def working_dir
|
51
|
+
@working_dir ||= File.realpath(path)
|
52
|
+
end
|
53
|
+
|
54
|
+
def executable
|
55
|
+
@executable ||= File.join(dist_folder, 'bin', 'elasticsearch')
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def download_file
|
61
|
+
return if downloaded?
|
62
|
+
open(final_path, 'wb') do |target|
|
63
|
+
download_options = {
|
64
|
+
content_length_proc: ->(t) { build_progress_bar(t) },
|
65
|
+
progress_proc: ->(s) { increment_progress(s) }
|
66
|
+
}
|
67
|
+
# direct call here to avoid spec issues with Kernel#open
|
68
|
+
distfile = OpenURI.open_uri("https://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-#{version}.zip", download_options)
|
69
|
+
target << distfile.read
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_file
|
74
|
+
return if extracted?
|
75
|
+
Dir.chdir(working_dir) do
|
76
|
+
# Extract archive in path, after block CWD is restored
|
77
|
+
Zip::File.open(final_path) do |zip_file|
|
78
|
+
# Extract all entries into working dir
|
79
|
+
zip_file.each(&:extract)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
# ensure main executable has execute permission
|
83
|
+
File.chmod(0755, executable)
|
84
|
+
# Create folder for log files
|
85
|
+
FileUtils.mkdir(File.join(dist_folder, 'logs'))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Build a progress bar to download elasticsearch
|
89
|
+
def build_progress_bar(total)
|
90
|
+
if total && total.to_i > 0
|
91
|
+
@download_progress_bar = ProgressBar.create title: "Downloading elasticsearch #{version}", total: total,
|
92
|
+
format: '%t |%bᗧ%i| %p%% (%r KB/sec) %e', progress_mark: ' ', remainder_mark: '・',
|
93
|
+
rate_scale: ->(rate) { rate / 1024 }, smoothing: 0.7
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def increment_progress(size)
|
98
|
+
@download_progress_bar.progress = size if @download_progress_bar
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'logging'
|
2
|
+
|
3
|
+
module Elasticsearch
|
4
|
+
module Embedded
|
5
|
+
|
6
|
+
# Contains stuff related to logging configuration
|
7
|
+
module LoggerConfiguration
|
8
|
+
|
9
|
+
# Configure logger verbosity for ::Elasticsearch::Embedded log hierarchy
|
10
|
+
# @param [String,Fixnum] level accepts strings levels or numbers
|
11
|
+
def verbosity(level)
|
12
|
+
level = level.to_s.downcase # normalize string to downcase
|
13
|
+
case
|
14
|
+
when level =~ /\A\d\Z/
|
15
|
+
Logging.logger[self].level = level.to_i
|
16
|
+
when Logging::LEVELS.include?(level)
|
17
|
+
Logging.logger[self].level = Logging::LEVELS[level]
|
18
|
+
else
|
19
|
+
Logging.logger[self].level = :info
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
# Clear all logging appenders for ::Elasticsearch::Embedded log hierarchy
|
25
|
+
def mute!
|
26
|
+
Logging.logger[self].clear_appenders
|
27
|
+
end
|
28
|
+
|
29
|
+
# @see https://github.com/TwP/logging/blob/master/examples/colorization.rb
|
30
|
+
def configure_logging!
|
31
|
+
# Configure logger levels for hierarchy
|
32
|
+
Logging.logger[self].level = :info
|
33
|
+
# Register a color scheme named bright
|
34
|
+
Logging.color_scheme 'bright', {
|
35
|
+
levels: {
|
36
|
+
info: :green,
|
37
|
+
warn: :yellow,
|
38
|
+
error: :red,
|
39
|
+
fatal: [:white, :on_red]
|
40
|
+
},
|
41
|
+
date: :blue,
|
42
|
+
logger: :cyan,
|
43
|
+
message: :white,
|
44
|
+
}
|
45
|
+
pattern_options = {pattern: '[%d] %-5l %c: %m\n'}
|
46
|
+
# Apply colors only if in tty
|
47
|
+
pattern_options[:color_scheme] = 'bright' if STDOUT.tty?
|
48
|
+
# Create a named appender to be used only for current module
|
49
|
+
Logging.logger[self].appenders = Logging.appenders.stdout(self.to_s, layout: Logging.layouts.pattern(pattern_options))
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Extension callback
|
55
|
+
def self.extended(base)
|
56
|
+
base.configure_logging!
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Elasticsearch
|
2
|
+
module Embedded
|
3
|
+
|
4
|
+
module RSpec
|
5
|
+
|
6
|
+
module ElasticSearchHelpers
|
7
|
+
# Return a client connected to the configured client, if ::Elasticsearch::Client is defined
|
8
|
+
# return a client, else return a URI attached to cluster.
|
9
|
+
# @see http://ruby-doc.org/stdlib-2.1.2/libdoc/net/http/rdoc/Net/HTTP.html
|
10
|
+
# @see https://github.com/elasticsearch/elasticsearch-ruby
|
11
|
+
def client
|
12
|
+
@client ||=
|
13
|
+
case
|
14
|
+
when defined?(::Elasticsearch::Client)
|
15
|
+
::Elasticsearch::Client.new host: "localhost:#{cluster.port}"
|
16
|
+
else
|
17
|
+
URI("http://localhost:#{cluster.port}/")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return a cluster instance to be used in tests
|
22
|
+
def cluster
|
23
|
+
ElasticSearchHelpers.memoized_cluster
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
|
28
|
+
# Return a singleton instance of cluster object
|
29
|
+
def memoized_cluster
|
30
|
+
@cluster ||= ::Elasticsearch::Embedded::Cluster.new
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
class << self
|
38
|
+
|
39
|
+
# Configure rspec for usage with ES cluster
|
40
|
+
def configure_with(*meta)
|
41
|
+
# assign default value to tags
|
42
|
+
::RSpec.configure do |config|
|
43
|
+
|
44
|
+
# Include helpers only in tagged specs
|
45
|
+
config.include ElasticSearchHelpers, *meta
|
46
|
+
|
47
|
+
# Before hook, starts the cluster
|
48
|
+
config.before(:each, *meta) do
|
49
|
+
ElasticSearchHelpers.memoized_cluster.ensure_started!
|
50
|
+
ElasticSearchHelpers.memoized_cluster.delete_all_indices!
|
51
|
+
end
|
52
|
+
|
53
|
+
# After suite hook, stop the cluster
|
54
|
+
config.after(:suite) do
|
55
|
+
ElasticSearchHelpers.memoized_cluster.stop if ElasticSearchHelpers.memoized_cluster.running?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Default config method, configure RSpec with :elasticsearch filter only.
|
61
|
+
# Equivalent to .configure_with(:elasticsearch)
|
62
|
+
def configure
|
63
|
+
configure_with(:elasticsearch)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
describe Elasticsearch::Embedded::Cluster, :elasticsearch do
|
2
|
+
|
3
|
+
describe '#clear_all_indices!' do
|
4
|
+
|
5
|
+
it 'should delete all indices' do
|
6
|
+
# Create a document and an index
|
7
|
+
client.index index: 'test', type: 'test-type', id: 1, body: {title: 'Test'}
|
8
|
+
expect {
|
9
|
+
cluster.delete_all_indices!
|
10
|
+
}.to change { client.indices.get_settings.count }.to(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should delete all indices' do
|
14
|
+
# Create a document and an index
|
15
|
+
client.index index: 'test', type: 'test-type', id: 1, body: {title: 'Test'}
|
16
|
+
client.index index: 'test2', type: 'test-type', id: 1, body: {title: 'Test'}
|
17
|
+
expect {
|
18
|
+
cluster.delete_index! 'test'
|
19
|
+
}.to change { client.indices.get_settings.keys }.to(['test2'])
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#pids' do
|
25
|
+
|
26
|
+
it 'should return a list running instances pids' do
|
27
|
+
expect(cluster.pids).to_not be_empty
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should return an empty array when not started' do
|
31
|
+
not_started = Elasticsearch::Embedded::Cluster.new
|
32
|
+
not_started.port = 50000 # Likely there's no process running on this port
|
33
|
+
expect(not_started.pids).to eq([])
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'Development template' do
|
39
|
+
|
40
|
+
before do
|
41
|
+
# It's not applied by default on non persistent clusters
|
42
|
+
cluster.apply_development_template!
|
43
|
+
client.indices.create index: 'any_index'
|
44
|
+
end
|
45
|
+
|
46
|
+
let(:index_settings) { client.indices.get_settings(index: 'any_index')['any_index']['settings']['index'] }
|
47
|
+
|
48
|
+
# Do not leave status across tests
|
49
|
+
after(:all) do
|
50
|
+
client.indices.delete_template name: 'development_template' if client.indices.get_template.has_key?('development_template')
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should configure 1 shard for each index' do
|
54
|
+
expect(index_settings).to include('number_of_shards' => '1')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should configure 0 replicas for each index' do
|
58
|
+
expect(index_settings).to include('number_of_replicas' => '0')
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe 'Cluster persistency' do
|
64
|
+
|
65
|
+
let(:persistent_cluster) { Elasticsearch::Embedded::Cluster.new }
|
66
|
+
let(:port) { cluster.port + 10 }
|
67
|
+
let(:client) { Elasticsearch::Client.new host: "localhost:#{port}" }
|
68
|
+
|
69
|
+
before do
|
70
|
+
persistent_cluster.persistent = true
|
71
|
+
persistent_cluster.cluster_name = 'persistent_cluster'
|
72
|
+
persistent_cluster.port = port
|
73
|
+
end
|
74
|
+
|
75
|
+
after do
|
76
|
+
# Ensure additional cluster is stopped when test is finished
|
77
|
+
persistent_cluster.stop if persistent_cluster.running?
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should persist data across restarts' do
|
81
|
+
persistent_cluster.start
|
82
|
+
# Index a document and trigger persistent index creation
|
83
|
+
client.index index: 'persistent', type: 'test-type', id: 1, body: {title: 'Test'}, refresh: true
|
84
|
+
# Restart cluster and check index presence
|
85
|
+
expect {
|
86
|
+
persistent_cluster.stop
|
87
|
+
persistent_cluster.start
|
88
|
+
}.to_not change { client.indices.get_settings }
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
describe Elasticsearch::Embedded::Downloader do
|
2
|
+
|
3
|
+
describe 'configuration using env' do
|
4
|
+
|
5
|
+
it 'should allow configuration through ENV' do
|
6
|
+
ENV['ELASTICSEARCH_VERSION'] = '1.3.3'
|
7
|
+
ENV['ELASTICSEARCH_DOWNLOAD_PATH'] = '/tmp'
|
8
|
+
d = Elasticsearch::Embedded::Downloader.new
|
9
|
+
expect(d.version).to eq '1.3.3'
|
10
|
+
expect(d.path).to eq '/tmp'
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
ENV['ELASTICSEARCH_VERSION'] = nil
|
15
|
+
ENV['ELASTICSEARCH_DOWNLOAD_PATH'] = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'configuration using options' do
|
21
|
+
|
22
|
+
it 'should allow configuration using option arguments' do
|
23
|
+
d = Elasticsearch::Embedded::Downloader.new(path: '/tmp', version: '1.3.3')
|
24
|
+
expect(d.version).to eq '1.3.3'
|
25
|
+
expect(d.path).to eq '/tmp'
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'default configuration' do
|
31
|
+
|
32
|
+
it 'should use default values' do
|
33
|
+
d = Elasticsearch::Embedded::Downloader.new
|
34
|
+
expect(d.version).to eq Elasticsearch::Embedded::Downloader::DEFAULT_VERSION
|
35
|
+
expect(d.path).to eq Elasticsearch::Embedded::Downloader::TEMPORARY_PATH
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
# Integration tests
|
41
|
+
context 'with mocked filesystem' do
|
42
|
+
|
43
|
+
# Stub filesystem calls
|
44
|
+
include FakeFS::SpecHelpers
|
45
|
+
|
46
|
+
let(:fake_archive) do
|
47
|
+
directory = "/elasticsearch-#{subject.version}"
|
48
|
+
archive = "/elasticsearch-#{subject.version}.zip"
|
49
|
+
FileUtils.mkdir_p(directory)
|
50
|
+
FileUtils.touch(File.join(directory, 'README.textile'))
|
51
|
+
FileUtils.mkdir_p(File.join(directory, 'bin'))
|
52
|
+
FileUtils.touch(File.join(directory, 'bin', 'elasticsearch'))
|
53
|
+
# Creates a zip archive with empty files
|
54
|
+
Zip::File.open(archive, Zip::File::CREATE) do |zipfile|
|
55
|
+
# make root folder of zip archive
|
56
|
+
zipfile.add(directory.sub('/', ''), directory)
|
57
|
+
# Add all content
|
58
|
+
Dir[File.join(directory, '**', '**')].each do |file|
|
59
|
+
zipfile.add(file.sub('/', ''), file)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
# Return the built archive file name
|
63
|
+
archive
|
64
|
+
end
|
65
|
+
|
66
|
+
# Realpath is needed because mockfs does not implement realpath
|
67
|
+
let(:path) { File.realpath(Dir.tmpdir) }
|
68
|
+
let(:zip_file) { File.join(path, "elasticsearch-#{subject.version}.zip") }
|
69
|
+
let(:dist_folder) { File.join(path, "elasticsearch-#{subject.version}") }
|
70
|
+
let(:executable) { File.join(dist_folder, 'bin', 'elasticsearch') }
|
71
|
+
subject { Elasticsearch::Embedded::Downloader.new(path: path) }
|
72
|
+
|
73
|
+
before do
|
74
|
+
# This is needed on OS X where /tmp is a symlink otherwise the
|
75
|
+
# openuri call will fail with Errno::ENOENT when trying to create a temporary file
|
76
|
+
FileUtils.mkdir_p(Dir.tmpdir)
|
77
|
+
# Ensure download path is present on the stubbed filesystem
|
78
|
+
FileUtils.mkdir_p(path)
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'file download' do
|
82
|
+
|
83
|
+
it 'should download file into configured path' do
|
84
|
+
expect(File.exists?(zip_file)).to be_falsey
|
85
|
+
# Stub only actual download of file
|
86
|
+
expect(OpenURI).to receive(:open_uri).and_return(File.open(fake_archive))
|
87
|
+
subject.perform
|
88
|
+
expect(File.size(zip_file)).to be >= 0
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should not download file if already present' do
|
92
|
+
FileUtils.touch(zip_file) # Simulate presence of downloaded version
|
93
|
+
FileUtils.mkdir_p(dist_folder) # Simulate presence of extracted version
|
94
|
+
expect { subject.perform }.to_not change { File.size(zip_file) }
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
describe 'file extraction' do
|
100
|
+
|
101
|
+
before do
|
102
|
+
# create the zip archive as if it were downloaded
|
103
|
+
expect(File.exists?(fake_archive)).to be_truthy
|
104
|
+
FileUtils.cp fake_archive, zip_file
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should extract zip archive when into final folder' do
|
108
|
+
expect { subject.perform }.to change { Dir[File.join(dist_folder, '**', '**')].count }
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'should not extract zip archive when final folder is already present' do
|
112
|
+
FileUtils.mkdir_p dist_folder
|
113
|
+
expect { subject.perform }.to_not change { Dir[File.join(dist_folder, '**', '**')] }
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should make bin/elasticsearch executable' do
|
117
|
+
subject.perform
|
118
|
+
expect(File.executable?(executable)).to be_truthy
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'should create a folder for log files' do
|
122
|
+
expect {
|
123
|
+
subject.perform
|
124
|
+
}.to change { File.directory?(File.join(dist_folder, 'logs')) }
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|