elasticsearch-embedded 0.1.0
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/.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
|