cauchy 0.1.1

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: acfe1f9c387508642988f41f47e65b0ea809b6c5
4
+ data.tar.gz: 4b3868c009a60a3217ea04589876537ee73dfee2
5
+ SHA512:
6
+ metadata.gz: dada688d444f5079b1a7a273f9d2a732d99cc5d61f1c2359edd3864fa3139c7f2a80cafb3091e9f6b0056d9cb5f4381e1d7973cd054aa3627ef6f4599903f744
7
+ data.tar.gz: 1def7a74c1ccc400dcbeedbda077538a88b75f8de90983718390e27d93a565e0e00fc27e84f4b86c58cdfed76682d4af8f38f3b788e46a3eb5b8fc5629302920
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .bundle
12
+ vendor/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ cache: bundler
3
+ rvm:
4
+ - 2.2.5
5
+ services:
6
+ - elasticsearch
7
+ env:
8
+ global:
9
+ - "ELASTICSEARCH_HOSTS=http://localhost:9200,http://127.0.0.1:9200"
10
+ script: bundle exec rake spec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Zinc Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Cauchy
2
+
3
+ [![Build Status](https://api.travis-ci.com/cotap/cauchy.svg?token=PQCDXv39wYQSK1Gnq72A&branch=master)](https://magnum.travis-ci.com/cotap/cauchy)
4
+
5
+ Cauchy manages your ES Index Schemas.
6
+
7
+ ## Installation
8
+
9
+ Install the Cauchy gem globally with
10
+
11
+ $ gem install cauchy
12
+
13
+ And verify your install with
14
+
15
+ $ cauchy version
16
+
17
+ ## Usage
18
+
19
+ ### Setup
20
+
21
+ To get started, create a new Cauchy project structure
22
+
23
+ $ cauchy init --path=/some/place/special
24
+
25
+ This will create the following structure
26
+
27
+ ```
28
+ ├── config.yml
29
+ └── schema
30
+ ```
31
+
32
+ Configure Cauchy's `config.yml` with your elasticsearch settings. You can find more information about the available options at the [ElasticSearch Transport docs](http://www.rubydoc.info/gems/elasticsearch-transport).
33
+
34
+ ### Creating an Index Schema
35
+
36
+ To create a new index schema, simply
37
+
38
+ $ cauchy new [index_schema_name]
39
+
40
+ This will generate a new template schema file located in your `schemas/` directory.
41
+
42
+ ### Applying your schema
43
+
44
+ Once you've configuring your schema, you're ready to apply it to ES with
45
+
46
+ $ cauchy apply [index_schema_name]
47
+
48
+ __Note:__ the schema name is optional here, and if left blank, Cauchy will run against all your defined schemas
49
+
50
+ ## Development
51
+
52
+ After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests.
53
+
54
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
55
+
56
+ ## Contributing
57
+
58
+ Bug reports and pull requests are welcome on GitHub at https://github.com/cotap/cauchy.
59
+
60
+ ## License
61
+
62
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
63
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
data/bin/cauchy ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'cauchy/cli'
5
+
6
+ trap('INT') { puts ''; exit }
7
+
8
+ Cauchy::Cli.start ARGV
data/cauchy.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cauchy/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'cauchy'
8
+ s.version = Cauchy::VERSION
9
+ s.authors = ['Paul Hamera', 'Evan Owen']
10
+ s.email = ['paul@zinc.it', 'evan@zinc.it']
11
+
12
+ s.summary = 'An elasticsearch schema management tool'
13
+ s.description = 'An elasticsearch schema management tool'
14
+ s.homepage = 'https://github.com/cotap'
15
+ s.license = 'MIT'
16
+
17
+ s.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ s.executables = Dir['bin/*'].map{ |f| File.basename(f) }
19
+ s.require_paths = ['lib']
20
+
21
+ s.required_ruby_version = '>= 2.0.0'
22
+
23
+ s.add_dependency 'activesupport', '>= 4.2'
24
+ s.add_dependency 'diffy', '~> 3.1'
25
+ s.add_dependency 'elasticsearch', '~> 1.0'
26
+ s.add_dependency 'indentation', '~> 0.0'
27
+ s.add_dependency 'json', '>= 1.8'
28
+ s.add_dependency 'rainbow', '~> 2.1'
29
+ s.add_dependency 'ruby-progressbar', '~> 1.8'
30
+ s.add_dependency 'thor', '~> 0.18'
31
+
32
+ s.add_development_dependency 'bundler', '~> 1.11'
33
+ s.add_development_dependency 'rake', '~> 10.0'
34
+ s.add_development_dependency 'rspec', '~> 3.0'
35
+ s.add_development_dependency 'rspec-its', '~> 1.2'
36
+ s.add_development_dependency 'pry'
37
+ end
data/lib/cauchy/cli.rb ADDED
@@ -0,0 +1,158 @@
1
+ require 'active_support/core_ext/hash/slice'
2
+ require 'active_support/core_ext/string/inflections'
3
+ require 'active_support/hash_with_indifferent_access'
4
+ require 'thor'
5
+ require 'yaml'
6
+
7
+ require 'cauchy'
8
+
9
+ module Cauchy
10
+
11
+ class Cli < Thor
12
+ include Thor::Actions
13
+
14
+ default_path = ENV.fetch('CAUCHY_PATH', '.')
15
+
16
+ class_option :path, default: default_path,
17
+ banner: 'project path', desc: 'CAUCHY_PATH env var will be recognized'
18
+
19
+ desc 'init', 'Create cauchy project directories'
20
+ def init
21
+ init_project
22
+ end
23
+
24
+ desc 'new [SCHEMA_NAME]', 'Create an empty index schema file'
25
+ def new(name)
26
+ verify_project!
27
+ generate_schema name
28
+ end
29
+
30
+ desc 'apply [SCHEMA_NAME]', 'Applys a schema'
31
+ method_option :reindex, default: false, type: :boolean, desc: 'Reindex data, if required'
32
+ method_option :close_index, default: false, type: :boolean, desc: 'Close index to update non-dynamic settings'
33
+ def apply(schema_name = nil)
34
+ verify_project!
35
+ Migrator.migrate(
36
+ client, schema_path, schema_name,
37
+ options.slice('reindex', 'close_index').symbolize_keys
38
+ )
39
+ rescue Elastic::CannotUpdateNonDynamicSettingsError => e
40
+ Cauchy.logger.warn e.to_s
41
+ Cauchy.logger.warn 'Provide --close-index in order to perform this update'
42
+ rescue MigrationError => e
43
+ Cauchy.logger.warn e.to_s
44
+ end
45
+
46
+ desc 'status [SCHEMA_NAME]', 'Displays index status'
47
+ def status(schema_name = nil)
48
+ verify_project!
49
+ Migrator.status(client, schema_path, schema_name)
50
+ rescue MigrationError => e
51
+ Cauchy.logger.warn e.to_s
52
+ end
53
+
54
+ desc 'version', 'Displays the version number'
55
+ def version
56
+ Cauchy.logger.info "Couchy v#{Cauchy::VERSION}"
57
+ end
58
+
59
+ private
60
+
61
+ def project_path
62
+ @project_path ||= File.expand_path options['path']
63
+ end
64
+
65
+ def config_path
66
+ @config_path ||= File.join project_path, 'config.yml'
67
+ end
68
+
69
+ def schema_path(schema_file = nil)
70
+ File.join *[project_path, 'schema', schema_file].compact
71
+ end
72
+
73
+ def client
74
+ @client ||= Elastic::Client.new(config)
75
+ end
76
+
77
+ def config
78
+ @config ||= begin
79
+ if File.exists?(config_path)
80
+ YAML.load_file(config_path).deep_symbolize_keys
81
+ else
82
+ { url: ENV.fetch('ELASTICSEARCH_URL', 'localhost:9200') }
83
+ end
84
+ end
85
+ end
86
+
87
+ def yaml_config
88
+
89
+ end
90
+
91
+ def normalize_schema_name(name)
92
+ name.gsub(/[^a-z0-9\-]+/i, '_').downcase
93
+ end
94
+
95
+ def verify_project!
96
+ unless Dir.exists? schema_path
97
+ Cauchy.logger.fatal "Unable to locate schema directory #{schema_path}"
98
+ Cauchy.logger.info 'Did you setup a project with `cauchy init`?'
99
+ exit
100
+ end
101
+ end
102
+
103
+ def init_project
104
+ unless Dir.exists? schema_path
105
+ Cauchy.logger.debug "Creating schema directory at #{schema_path}"
106
+ FileUtils.mkdir_p schema_path
107
+ end
108
+
109
+ unless File.exists? config_path
110
+ Cauchy.logger.debug "Creating config file at #{config_path}"
111
+ File.open(config_path, 'w+') { |f| f.print <<-YAML }
112
+ # Supports elasticsearch/transport options. See: http://www.rubydoc.info/gems/elasticsearch-transport
113
+ # This file is optional, you can also set ELASTICSEARCH_URL in your environment
114
+ hosts:
115
+ - host: localhost
116
+ port: 9200
117
+ scheme: http
118
+ YAML
119
+ end
120
+
121
+ Cauchy.logger.info "\nWant to run cauchy from anywhere? Add the following to your shell config:"
122
+ Cauchy.logger.debug "export CAUCHY_PATH=#{project_path}"
123
+ end
124
+
125
+ def generate_schema(name)
126
+ name = normalize_schema_name(name)
127
+ path = schema_path("#{name}.rb")
128
+
129
+ if File.exists? path
130
+ return unless yes? "Schema file exists at #{path}, overwrite? (y/n)?"
131
+ end
132
+
133
+ Cauchy.logger.info "Creating a new schema `#{name}` at #{path}"
134
+
135
+ File.open(path, 'w+') { |f| f.print <<-RB }
136
+ Cauchy::IndexSchema.define(:#{name}) do
137
+
138
+ settings do
139
+ # {
140
+ # number_of_replicas: 2
141
+ # }
142
+ end
143
+
144
+ mappings do
145
+ # {
146
+ # #{name.singularize}: {
147
+ # properties: {
148
+ # name: { type: 'string' }
149
+ # }
150
+ # }
151
+ # }
152
+ end
153
+
154
+ end
155
+ RB
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,29 @@
1
+ require 'elasticsearch'
2
+
3
+ require 'cauchy/elastic/index'
4
+
5
+ module Cauchy
6
+ module Elastic
7
+ class Client
8
+
9
+ attr_reader :server
10
+
11
+ def initialize(config)
12
+ @server = Elasticsearch::Client.new(config)
13
+ end
14
+
15
+ def index(name)
16
+ Index.new(server, name)
17
+ end
18
+
19
+ def bulk(body)
20
+ server.bulk body: body
21
+ end
22
+
23
+ def update_aliases(body)
24
+ server.indices.update_aliases body: body
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,115 @@
1
+ module Cauchy
2
+ module Elastic
3
+
4
+ class ElasticError < StandardError
5
+ def initialize(message = nil)
6
+ super
7
+ end
8
+ end
9
+
10
+ class IndexAlreadyExistsError < ElasticError
11
+ def initialize(name)
12
+ super("Index \"#{name}\" already exists")
13
+ end
14
+ end
15
+
16
+ class CannotUpdateNonDynamicSettingsError < ElasticError
17
+ def initialize(name)
18
+ super("Index \"#{name}\" cannot be updated while open.")
19
+ end
20
+ end
21
+
22
+ class Index
23
+
24
+ attr_reader :server, :name
25
+
26
+ def initialize(server, name)
27
+ @server = server
28
+ @name = name
29
+ end
30
+
31
+ def exists?
32
+ server.indices.exists? index: name
33
+ end
34
+
35
+ def aliases
36
+ get 'aliases'
37
+ end
38
+
39
+ def mappings
40
+ get('mapping')[name]['mappings']
41
+ rescue
42
+ {}
43
+ end
44
+
45
+ def settings
46
+ get('settings')[name]['settings']['index']
47
+ rescue
48
+ {}
49
+ end
50
+
51
+ def alias=(alias_name)
52
+ server.indices.put_alias index: name, name: alias_name
53
+ end
54
+
55
+ def mappings=(mappings)
56
+ mappings.each do |type, mapping|
57
+ server.indices.put_mapping index: name, type: type,
58
+ body: { type => mapping }
59
+ end
60
+ end
61
+
62
+ def settings=(settings)
63
+ server.indices.put_settings index: name, body: settings
64
+ rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
65
+ if e.message =~ /update non dynamic settings/
66
+ raise CannotUpdateNonDynamicSettingsError.new(name)
67
+ else
68
+ raise
69
+ end
70
+ end
71
+
72
+ def create(settings: {}, mappings: {})
73
+ server.indices.create index: name,
74
+ body: { settings: settings, mappings: mappings }
75
+ rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e
76
+ if e.message =~ /IndexAlreadyExists/
77
+ raise IndexAlreadyExistsError.new(name)
78
+ else
79
+ raise
80
+ end
81
+ end
82
+
83
+ def delete
84
+ server.indices.delete index: name
85
+ end
86
+
87
+ def open
88
+ server.indices.open index: name
89
+ end
90
+
91
+ def close
92
+ server.indices.close index: name
93
+ end
94
+
95
+ def scroll(options = {})
96
+ options = { index: name, search_type: 'scan', scroll: '5m', size: 100 }.merge(options)
97
+ scroll_id = server.search(options)['_scroll_id']
98
+ begin
99
+ results = server.scroll(scroll_id: scroll_id, scroll: options[:scroll])
100
+ documents, scroll_id = results['hits']['hits'], results['_scroll_id']
101
+ yield documents, results['hits']['total'] if documents.any?
102
+ end while documents.size > 0
103
+ end
104
+
105
+ private
106
+
107
+ def get(resource)
108
+ server.indices.send(['get', resource].join('_').to_sym, index: name)
109
+ rescue Elasticsearch::Transport::Transport::Errors::NotFound
110
+ nil
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_support/core_ext/hash'
2
+
3
+ module Cauchy
4
+ class IndexSchema
5
+ module Normalization
6
+
7
+ IGNORE_SETTINGS = %w[
8
+ creation_date
9
+ uuid
10
+ version
11
+ ]
12
+
13
+ def normalize_mapping(hash)
14
+ hash.deep_stringify_keys.sort.map do |key, field|
15
+ if field.key?('properties')
16
+ field['properties'] = normalize_mapping(field['properties'])
17
+ field['type'] = 'object'
18
+ end
19
+
20
+ if field['type'] == 'date'
21
+ field['format'] ||= 'dateOptionalTime'
22
+ end
23
+
24
+ if ['boolean', 'long', 'double', 'date'].include?(field['type'])
25
+ field.delete('analyzer')
26
+ field['index'] ||= 'not_analyzed'
27
+ end
28
+
29
+ if key == 'properties'
30
+ field = normalize_mapping(field)
31
+ else
32
+ field = normalize_value(field)
33
+ end
34
+
35
+ [key, field]
36
+ end.to_h
37
+ end
38
+
39
+ def normalize_settings(hash)
40
+ normalize_value(hash.deep_stringify_keys.except(*IGNORE_SETTINGS))
41
+ end
42
+
43
+ def normalize_value(value)
44
+ case value
45
+ when Hash
46
+ value.sort.map {|key, v| [key, normalize_value(v)] }.to_h
47
+ when Numeric
48
+ value.to_s
49
+ else
50
+ value
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,77 @@
1
+ require 'cauchy/index_schema/normalization'
2
+
3
+ module Cauchy
4
+ class IndexSchema
5
+
6
+ include Normalization
7
+
8
+ @@schemas = {}
9
+
10
+ class << self
11
+
12
+ def define(index_alias, &block)
13
+ index_alias = index_alias.to_s
14
+ schemas[index_alias] = new(index_alias).tap { |s| s.define(&block) }
15
+ end
16
+
17
+ def load_schemas(paths)
18
+ Dir[*Array(paths).map { |p| "#{p}/**/*.rb" }].each { |f| load f }
19
+ end
20
+
21
+ def schemas
22
+ @@schemas
23
+ end
24
+
25
+ def schemas=(schemas)
26
+ @@schemas = schemas
27
+ end
28
+
29
+ end
30
+
31
+ attr_accessor :index_alias
32
+
33
+ def initialize(index_alias)
34
+ @index_alias = index_alias
35
+ end
36
+
37
+ def define(&block)
38
+ instance_eval(&block)
39
+ end
40
+
41
+ def mappings(&block)
42
+ self.mappings = block.call || {} if block_given?
43
+ @mappings || {}
44
+ end
45
+
46
+ def mappings=(value)
47
+ @mappings = value.map do |type, mapping|
48
+ [type.to_s, normalize_mapping(mapping)]
49
+ end.to_h
50
+ end
51
+
52
+ def mapping_for(type)
53
+ return unless mappings.key?(type)
54
+ mappings[type]['properties']
55
+ end
56
+
57
+ def settings(&block)
58
+ self.settings = block.call || {} if block_given?
59
+ @settings || {}
60
+ end
61
+
62
+ def settings=(value)
63
+ @settings = normalize_settings(value)
64
+ end
65
+
66
+ def types
67
+ mappings.keys
68
+ end
69
+
70
+ def version
71
+ @version ||= Digest::SHA1.hexdigest(
72
+ { settings: settings, mappings: mappings }.to_json
73
+ )
74
+ end
75
+
76
+ end
77
+ end
@@ -0,0 +1,58 @@
1
+ require 'delegate'
2
+ require 'indentation'
3
+ require 'rainbow/ext/string'
4
+ require 'logger'
5
+
6
+ module Cauchy
7
+ module_function
8
+
9
+ class << self
10
+
11
+ class PrettyLogger < SimpleDelegator
12
+ def initialize logger
13
+ super
14
+
15
+ old_formatter = logger.formatter
16
+
17
+ logger.formatter = proc do |level, time, prog, msg|
18
+ unless msg.start_with?("\e")
19
+ color = case level
20
+ when 'FATAL' then :red
21
+ when 'WARN' then :yellow
22
+ when 'INFO' then :blue
23
+ when 'DEBUG' then '333333'
24
+ else :default
25
+ end
26
+ msg = msg.color(color)
27
+ end
28
+
29
+ old_formatter.call level, time, prog, msg
30
+ end
31
+ end
32
+
33
+ %w[ debug info warn fatal ].each do |level|
34
+ define_method level do |msg, opts = {}|
35
+ __getobj__.__send__ level, msg
36
+ end
37
+ end
38
+
39
+ def inspect object
40
+ info object.to_yaml[4..-1].strip.indent
41
+ end
42
+ end
43
+
44
+ def logger=(logger)
45
+ @logger = logger
46
+ end
47
+
48
+ def logger
49
+ @logger ||= begin
50
+ logger = Logger.new STDOUT
51
+ logger.level = Logger::DEBUG
52
+ logger.formatter = proc { |_, _, _, msg| "#{msg}\n" }
53
+ PrettyLogger.new logger
54
+ end
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,82 @@
1
+ require 'diffy'
2
+
3
+ module Cauchy
4
+ class Migration
5
+
6
+ CREATION_SETTINGS = %w[
7
+ codec
8
+ number_of_shards
9
+ ]
10
+
11
+ attr_accessor :schema, :new_schema
12
+
13
+ def initialize(schema, new_schema)
14
+ @schema = schema
15
+ @new_schema = new_schema
16
+ end
17
+
18
+ def changes_mappings?
19
+ (new_schema.types - schema.types).any? ||
20
+ (new_schema.types & schema.types).any? do |type|
21
+ mapping, new_mapping = schema.mapping_for(type), new_schema.mapping_for(type)
22
+ removed_fields = mapping.keys - new_mapping.keys
23
+ mapping.except(*removed_fields) != new_mapping
24
+ end
25
+ end
26
+
27
+ def changes_existing_mappings?
28
+ (new_schema.types & schema.types).any? do |type|
29
+ mapping, new_mapping = schema.mapping_for(type), new_schema.mapping_for(type)
30
+ common_fields = mapping.keys & new_mapping.keys
31
+ mapping.slice(*common_fields) != new_mapping.slice(*common_fields)
32
+ end
33
+ end
34
+
35
+ def changed_settings
36
+ new_schema.settings.select do |name, setting|
37
+ schema.settings[name] != setting
38
+ end.to_h
39
+ end
40
+
41
+ def changes_settings?
42
+ changed_settings.present?
43
+ end
44
+
45
+ def changes_creation_settings?
46
+ schema.settings.slice(*CREATION_SETTINGS).except(*removed_settings) !=
47
+ new_schema.settings.slice(*CREATION_SETTINGS)
48
+ end
49
+
50
+ def removed_settings
51
+ schema.settings.keys - new_schema.settings.keys
52
+ end
53
+
54
+ def requires_reindex?
55
+ changes_creation_settings? || changes_existing_mappings?
56
+ end
57
+
58
+ def up_to_date?
59
+ !(changes_settings? || changes_mappings?)
60
+ end
61
+
62
+ def mappings_diffs
63
+ (schema.mappings.keys | new_schema.mappings.keys).map do |type|
64
+ diff = Diffy::Diff.new(
65
+ JSON.pretty_generate(schema.mappings[type] || {}) + "\n",
66
+ JSON.pretty_generate(new_schema.mappings[type] || {}) + "\n",
67
+ context: 3
68
+ )
69
+ [type, diff]
70
+ end.to_h
71
+ end
72
+
73
+ def settings_diff
74
+ Diffy::Diff.new(
75
+ JSON.pretty_generate(schema.settings.except(*removed_settings)) + "\n",
76
+ JSON.pretty_generate(new_schema.settings.except(*removed_settings)) + "\n",
77
+ context: 3
78
+ )
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,249 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'active_support/core_ext/object/blank'
3
+ require 'ruby-progressbar'
4
+ require 'json'
5
+
6
+ require 'cauchy/index_schema'
7
+ require 'cauchy/migration'
8
+
9
+ module Cauchy
10
+
11
+ class MigrationError < StandardError
12
+ end
13
+
14
+ class UnknownIndexSchemaError < MigrationError
15
+ def initialize(name)
16
+ super("No index schema defined for \"#{name}\"")
17
+ end
18
+ end
19
+
20
+ class MultipleIndexAliasError < MigrationError
21
+ def initialize(name)
22
+ super("Multiple index aliases found for \"#{name}\"")
23
+ end
24
+ end
25
+
26
+ class NoIndexSchemasError < MigrationError
27
+ def initialize
28
+ super("No index schemas defined")
29
+ end
30
+ end
31
+
32
+ class Migrator
33
+
34
+ class << self
35
+
36
+ def migrate(client, index_paths = nil, target_index = nil, options = {})
37
+ with_schemas(index_paths, target_index) do |schema|
38
+ new(client, schema).migrate options
39
+ end
40
+ end
41
+
42
+ def status(client, index_paths = nil, target_index = nil)
43
+ with_schemas(index_paths, target_index) do |schema|
44
+ new(client, schema).status
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def with_schemas(index_paths, target_index = nil)
51
+ found_target = false
52
+ IndexSchema.load_schemas(index_paths)
53
+
54
+ raise NoIndexSchemasError unless IndexSchema.schemas.any?
55
+
56
+ IndexSchema.schemas.each do |index_alias, schema|
57
+ if target_index.nil? || target_index == index_alias
58
+ found_target = true
59
+ yield schema
60
+ end
61
+ end
62
+
63
+ raise UnknownIndexSchemaError, target_index unless found_target
64
+ end
65
+
66
+ end
67
+
68
+ attr_reader :client, :schema
69
+
70
+ def initialize(client, schema)
71
+ @client = client
72
+ @schema = schema
73
+ end
74
+
75
+ delegate :index_alias, :version, to: :schema
76
+
77
+ def new_index_name
78
+ "#{index_alias}_#{version}"
79
+ end
80
+
81
+ def new_index
82
+ @new_index ||= client.index(new_index_name)
83
+ end
84
+
85
+ def old_index
86
+ @old_index ||= client.index(resolve_index)
87
+ end
88
+
89
+ def migration
90
+ @migration ||= Migration.new(old_schema, schema)
91
+ end
92
+
93
+ def migrate(options = {})
94
+ raise MultipleIndexAliasError, index_alias if index_aliases.keys.length > 1
95
+
96
+ if options[:reindex] || !old_index.exists?
97
+ return reindex(options)
98
+ end
99
+
100
+ if migration.up_to_date?
101
+ log 'Index is up-to-date.'
102
+ return true
103
+ end
104
+
105
+ if migration.requires_reindex?
106
+ log migration_diff, :unknown
107
+ log 'Requires reindexing!', :warn
108
+ return false
109
+ end
110
+
111
+ if migration.changes_settings?
112
+ log 'Updating index settings... ' do
113
+ begin
114
+ old_index.close if options[:close_index]
115
+ old_index.settings = migration.changed_settings
116
+ rescue Elastic::CannotUpdateNonDynamicSettingsError
117
+ log migration_diff, :unknown
118
+ raise
119
+ ensure
120
+ old_index.open if options[:close_index]
121
+ end
122
+ 'done.'
123
+ end
124
+ end
125
+
126
+ if migration.changes_mappings?
127
+ log 'Updating index mappings... ' do
128
+ old_index.mappings = migration.new_schema.mappings
129
+ 'done.'
130
+ end
131
+ end
132
+ end
133
+
134
+ def reindex(options = {})
135
+ if old_index.exists?
136
+ if migration.up_to_date?
137
+ log 'Index is up-to-date.'
138
+ return true
139
+ elsif !migration.requires_reindex?
140
+ log 'Does not require reindexing... skipping.'
141
+ return false
142
+ end
143
+ end
144
+
145
+ begin
146
+ log "Creating new index #{new_index_name}"
147
+ new_index.create settings: schema.settings, mappings: schema.mappings
148
+ rescue Elastic::IndexAlreadyExistsError
149
+ log "Index #{new_index_name} already exists, continuing...", :warn
150
+ end
151
+
152
+ reindex_data if old_index.exists?
153
+
154
+ cleanup options
155
+
156
+ true
157
+ end
158
+
159
+ def reindex_data
160
+ log 'Reindexing...'
161
+ progress_bar = nil
162
+ old_index.scroll do |documents, total|
163
+ docs = documents.map do |h|
164
+ {
165
+ index: {
166
+ _index: new_index.name,
167
+ _type: h['_type'],
168
+ _id: h['_id'],
169
+ data: h['_source']
170
+ }
171
+ }
172
+ end
173
+
174
+ client.bulk docs
175
+
176
+ progress_bar ||= ProgressBar.create(total: total, throttle_rate: 0.1, format: '%a |%B| %e')
177
+ progress_bar.progress += docs.size
178
+ end
179
+ end
180
+
181
+ def status
182
+ if migration.up_to_date?
183
+ log 'Index is up-to-date.'
184
+ else
185
+ log migration_diff, :unknown
186
+ log 'Requires reindexing!', :warn if migration.requires_reindex?
187
+ end
188
+ end
189
+
190
+ def inspect
191
+ "#<#{self.class.name}"\
192
+ " index_alias=\"#{index_alias}\""\
193
+ " version=\"#{version[0..6]}\">"
194
+ end
195
+
196
+ def old_schema
197
+ @old_schema ||= begin
198
+ IndexSchema.new(index_alias).tap do |schema|
199
+ schema.mappings = old_index.mappings
200
+ schema.settings = old_index.settings
201
+ end
202
+ end
203
+ end
204
+
205
+ private
206
+
207
+ def cleanup(options)
208
+ log "Aliasing #{new_index.name} => #{index_alias}"
209
+ if old_index.name == index_alias
210
+ old_index.delete if old_index.exists?
211
+ new_index.alias = index_alias
212
+ else
213
+ client.update_aliases({
214
+ actions: [
215
+ { add: { alias: index_alias, index: new_index.name } },
216
+ { remove: { alias: index_alias, index: old_index.name } }
217
+ ]
218
+ })
219
+ old_index.delete if options.fetch :cleanup, true
220
+ end
221
+ end
222
+
223
+ def migration_diff
224
+ mappings_diff = migration.mappings_diffs.map do |type, diff|
225
+ diff = diff.to_s(:color).rstrip
226
+ "Mapping type=#{type}:\n" + diff if diff.present?
227
+ end.compact.join("\n")
228
+
229
+ settings_diff = migration.settings_diff.to_s(:color).rstrip
230
+ settings_diff = "Settings:\n" + settings_diff if settings_diff.present?
231
+
232
+ [mappings_diff, settings_diff].select(&:present?).join("\n\n")
233
+ end
234
+
235
+ def resolve_index
236
+ index_aliases.keys.first || index_alias
237
+ end
238
+
239
+ def index_aliases
240
+ client.index(index_alias).aliases
241
+ end
242
+
243
+ def log(msg, method = :info, &block)
244
+ Cauchy.logger.send(method, msg)
245
+ Cauchy.logger.send(method, block.call) if block_given?
246
+ end
247
+
248
+ end
249
+ end
@@ -0,0 +1,3 @@
1
+ module Cauchy
2
+ VERSION = '0.1.1'
3
+ end
data/lib/cauchy.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'cauchy/elastic/client'
2
+ require 'cauchy/migrator'
3
+ require 'cauchy/logging'
4
+ require 'cauchy/version'
metadata ADDED
@@ -0,0 +1,248 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cauchy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Paul Hamera
8
+ - Evan Owen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2016-12-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '4.2'
28
+ - !ruby/object:Gem::Dependency
29
+ name: diffy
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '3.1'
35
+ type: :runtime
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '3.1'
42
+ - !ruby/object:Gem::Dependency
43
+ name: elasticsearch
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: indentation
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - "~>"
61
+ - !ruby/object:Gem::Version
62
+ version: '0.0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "~>"
68
+ - !ruby/object:Gem::Version
69
+ version: '0.0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: json
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '1.8'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '1.8'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rainbow
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '2.1'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '2.1'
98
+ - !ruby/object:Gem::Dependency
99
+ name: ruby-progressbar
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '1.8'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '1.8'
112
+ - !ruby/object:Gem::Dependency
113
+ name: thor
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: '0.18'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: '0.18'
126
+ - !ruby/object:Gem::Dependency
127
+ name: bundler
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '1.11'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '1.11'
140
+ - !ruby/object:Gem::Dependency
141
+ name: rake
142
+ requirement: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - "~>"
145
+ - !ruby/object:Gem::Version
146
+ version: '10.0'
147
+ type: :development
148
+ prerelease: false
149
+ version_requirements: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - "~>"
152
+ - !ruby/object:Gem::Version
153
+ version: '10.0'
154
+ - !ruby/object:Gem::Dependency
155
+ name: rspec
156
+ requirement: !ruby/object:Gem::Requirement
157
+ requirements:
158
+ - - "~>"
159
+ - !ruby/object:Gem::Version
160
+ version: '3.0'
161
+ type: :development
162
+ prerelease: false
163
+ version_requirements: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - "~>"
166
+ - !ruby/object:Gem::Version
167
+ version: '3.0'
168
+ - !ruby/object:Gem::Dependency
169
+ name: rspec-its
170
+ requirement: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - "~>"
173
+ - !ruby/object:Gem::Version
174
+ version: '1.2'
175
+ type: :development
176
+ prerelease: false
177
+ version_requirements: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - "~>"
180
+ - !ruby/object:Gem::Version
181
+ version: '1.2'
182
+ - !ruby/object:Gem::Dependency
183
+ name: pry
184
+ requirement: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ">="
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ type: :development
190
+ prerelease: false
191
+ version_requirements: !ruby/object:Gem::Requirement
192
+ requirements:
193
+ - - ">="
194
+ - !ruby/object:Gem::Version
195
+ version: '0'
196
+ description: An elasticsearch schema management tool
197
+ email:
198
+ - paul@zinc.it
199
+ - evan@zinc.it
200
+ executables:
201
+ - cauchy
202
+ extensions: []
203
+ extra_rdoc_files: []
204
+ files:
205
+ - ".gitignore"
206
+ - ".rspec"
207
+ - ".travis.yml"
208
+ - Gemfile
209
+ - LICENSE.txt
210
+ - README.md
211
+ - Rakefile
212
+ - bin/cauchy
213
+ - cauchy.gemspec
214
+ - lib/cauchy.rb
215
+ - lib/cauchy/cli.rb
216
+ - lib/cauchy/elastic/client.rb
217
+ - lib/cauchy/elastic/index.rb
218
+ - lib/cauchy/index_schema.rb
219
+ - lib/cauchy/index_schema/normalization.rb
220
+ - lib/cauchy/logging.rb
221
+ - lib/cauchy/migration.rb
222
+ - lib/cauchy/migrator.rb
223
+ - lib/cauchy/version.rb
224
+ homepage: https://github.com/cotap
225
+ licenses:
226
+ - MIT
227
+ metadata: {}
228
+ post_install_message:
229
+ rdoc_options: []
230
+ require_paths:
231
+ - lib
232
+ required_ruby_version: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - ">="
235
+ - !ruby/object:Gem::Version
236
+ version: 2.0.0
237
+ required_rubygems_version: !ruby/object:Gem::Requirement
238
+ requirements:
239
+ - - ">="
240
+ - !ruby/object:Gem::Version
241
+ version: '0'
242
+ requirements: []
243
+ rubyforge_project:
244
+ rubygems_version: 2.4.5.1
245
+ signing_key:
246
+ specification_version: 4
247
+ summary: An elasticsearch schema management tool
248
+ test_files: []