mode 0.0.10 → 0.0.11
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 +4 -4
- data/.rspec +1 -0
- data/.travis.yml +1 -0
- data/bin/mode +3 -8
- data/lib/connect.rb +13 -0
- data/lib/mode.rb +18 -5
- data/lib/mode/api/link.rb +4 -0
- data/lib/mode/api/request.rb +20 -10
- data/lib/mode/auth/access_token.rb +16 -0
- data/lib/mode/cli/connect.rb +1 -1
- data/lib/mode/commands/analyze_field.rb +1 -3
- data/lib/mode/commands/connect.rb +24 -49
- data/lib/mode/commands/helpers.rb +23 -18
- data/lib/mode/commands/import.rb +2 -4
- data/lib/mode/commands/login.rb +88 -25
- data/lib/mode/config.rb +60 -2
- data/lib/mode/connector/connect.rb +11 -0
- data/lib/mode/connector/daemon.rb +54 -12
- data/lib/mode/connector/daemonizer.rb +107 -0
- data/lib/mode/connector/data_source.rb +36 -9
- data/lib/mode/connector/scheduler.rb +6 -5
- data/lib/mode/connector/uploader.rb +3 -4
- data/lib/mode/version.rb +1 -1
- data/mode.gemspec +7 -4
- data/spec/api/request_spec.rb +9 -6
- data/spec/commands/connect_spec.rb +45 -59
- data/spec/commands/helpers_spec.rb +9 -22
- data/spec/commands/import_spec.rb +5 -0
- data/spec/commands/login_spec.rb +87 -76
- data/spec/config_spec.rb +9 -0
- data/spec/connector/daemon_spec.rb +11 -6
- data/spec/connector/daemonizer_spec.rb +106 -0
- data/spec/connector/data_source_spec.rb +15 -17
- data/spec/connector/registrar_spec.rb +5 -6
- data/spec/connector/scheduler_spec.rb +3 -2
- data/spec/connector/uploader_spec.rb +9 -9
- metadata +65 -65
- data/lib/mode/configurable.rb +0 -46
- data/lib/mode/connector/config.rb +0 -31
- data/spec/connector/config_spec.rb +0 -46
data/lib/mode/commands/login.rb
CHANGED
@@ -23,8 +23,8 @@ module Mode
|
|
23
23
|
|
24
24
|
def execute
|
25
25
|
username, password = configure_credentials
|
26
|
-
|
27
|
-
update_configuration(environment, username,
|
26
|
+
access_token = choose_access_token
|
27
|
+
update_configuration(environment, username, access_token)
|
28
28
|
rescue => err
|
29
29
|
say err.message
|
30
30
|
Mode::Logger.instance.error(
|
@@ -36,9 +36,9 @@ module Mode
|
|
36
36
|
|
37
37
|
def configure_api(username, password)
|
38
38
|
Mode::API::Request.configure(environment, {
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
'credentials' => {
|
40
|
+
'username' => username,
|
41
|
+
'password' => password
|
42
42
|
}
|
43
43
|
})
|
44
44
|
end
|
@@ -54,13 +54,9 @@ module Mode
|
|
54
54
|
return username, password
|
55
55
|
end
|
56
56
|
|
57
|
-
def
|
58
|
-
resource = Mode::API::Request.get(:access_tokens)
|
59
|
-
|
57
|
+
def verify_resource(resource)
|
60
58
|
if resource.is_a?(Mode::API::Resource)
|
61
|
-
resource
|
62
|
-
Mode::Auth::AccessToken.new(access_token)
|
63
|
-
end
|
59
|
+
resource
|
64
60
|
elsif resource.status == 401
|
65
61
|
raise "Login failed, credentials invalid"
|
66
62
|
elsif resource.status == 404
|
@@ -72,35 +68,102 @@ module Mode
|
|
72
68
|
end
|
73
69
|
end
|
74
70
|
|
75
|
-
def
|
76
|
-
|
71
|
+
def create_access_token
|
72
|
+
name = ask "\nChoose a token name:"
|
73
|
+
|
74
|
+
resource = Mode::API::Request.post(
|
75
|
+
:access_tokens,
|
76
|
+
:access_token => { :name => name }
|
77
|
+
)
|
78
|
+
|
79
|
+
Mode::Auth::AccessToken.new(verify_resource(resource))
|
80
|
+
end
|
81
|
+
|
82
|
+
def fetch_access_tokens
|
83
|
+
resource = Mode::API::Request.get(:access_tokens)
|
84
|
+
tokens = verify_resource(resource).embedded('access_tokens')
|
85
|
+
|
86
|
+
tokens.map do |access_token|
|
87
|
+
Mode::Auth::AccessToken.new(access_token)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def verify_access_token(access_token, password)
|
92
|
+
if token = access_token.verify(password)
|
93
|
+
token
|
94
|
+
else
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def prompt_create_access_token
|
100
|
+
if yes? "\nNo API tokens found, would you like to create a new token? [y/n]"
|
101
|
+
create_access_token
|
102
|
+
else
|
103
|
+
raise "No API access tokens found or created"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def verify_access_token_secret(access_token)
|
108
|
+
token = nil
|
109
|
+
until token
|
110
|
+
password = ask "Please provide the access token password:"
|
111
|
+
token = verify_access_token(access_token, password)
|
112
|
+
say "API token verification failed, please try again." unless token
|
113
|
+
end
|
114
|
+
token
|
115
|
+
end
|
116
|
+
|
117
|
+
def ask_for_access_token_choice(access_tokens)
|
118
|
+
say "\nAvailable access tokens\n\n"
|
119
|
+
|
120
|
+
correct_answer = nil
|
121
|
+
until correct_answer
|
122
|
+
answer_set = [""]
|
123
|
+
access_tokens.each_with_index do |token, index|
|
124
|
+
answer_set << "#{index + 1}"
|
125
|
+
say "#{index + 1}. #{token.name}"
|
126
|
+
end
|
77
127
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
say
|
128
|
+
answer = ask "\nPlease choose an API token or [enter] to create a new token:"
|
129
|
+
|
130
|
+
correct_answer = answer_set.include?(answer) ? answer : nil
|
131
|
+
say("Your response must be one of the tokens or [enter]. Please try again.") unless correct_answer
|
132
|
+
end
|
133
|
+
correct_answer
|
134
|
+
end
|
135
|
+
|
136
|
+
def prompt_choose_access_token(access_tokens)
|
137
|
+
choice = ask_for_access_token_choice(access_tokens)
|
138
|
+
|
139
|
+
if choice.length == 0
|
140
|
+
create_access_token
|
141
|
+
else
|
142
|
+
verify_access_token_secret(access_tokens[choice.to_i - 1])
|
82
143
|
end
|
83
|
-
choice = ask "\nWhich token would you like to use?:", :limited_to => choices
|
84
144
|
end
|
85
145
|
|
86
146
|
def choose_access_token
|
87
147
|
access_tokens = fetch_access_tokens
|
88
148
|
|
89
|
-
if access_tokens.
|
90
|
-
|
91
|
-
elsif access_tokens.length == 1
|
92
|
-
access_tokens.first
|
149
|
+
if access_tokens.empty?
|
150
|
+
access_token = prompt_create_access_token
|
93
151
|
else
|
94
|
-
|
95
|
-
access_tokens[chosen.to_i - 1]
|
152
|
+
access_token = prompt_choose_access_token(access_tokens)
|
96
153
|
end
|
97
154
|
end
|
98
155
|
|
99
156
|
def update_configuration(environment, username, access_token)
|
100
157
|
config = find_or_create_config
|
158
|
+
|
101
159
|
config.username = username
|
102
|
-
config.access_token = access_token
|
103
160
|
config.environment = environment.to_s
|
161
|
+
|
162
|
+
config.access_token = {
|
163
|
+
'token' => access_token.token,
|
164
|
+
'password' => access_token.password
|
165
|
+
}
|
166
|
+
|
104
167
|
config.save
|
105
168
|
|
106
169
|
say "Updated configuration at #{config.path}"
|
data/lib/mode/config.rb
CHANGED
@@ -2,17 +2,69 @@ require 'yaml'
|
|
2
2
|
|
3
3
|
module Mode
|
4
4
|
class Config
|
5
|
-
|
5
|
+
attr_reader :path
|
6
6
|
|
7
7
|
# Config Variables
|
8
8
|
attr_accessor :username
|
9
9
|
attr_accessor :access_token
|
10
10
|
attr_accessor :environment
|
11
|
+
attr_accessor :data_sources
|
12
|
+
|
13
|
+
def initialize(path, filename = nil)
|
14
|
+
@path = self.class.full_path(path, filename)
|
15
|
+
|
16
|
+
if File.exist?(@path)
|
17
|
+
configure YAML.load_file(@path)
|
18
|
+
else
|
19
|
+
raise "Could not load configuration file from #{@path}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def save
|
24
|
+
File.open(path, 'w+') do |file|
|
25
|
+
file.write(to_yaml)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def access_token_key
|
30
|
+
access_token && access_token['token']
|
31
|
+
end
|
32
|
+
|
33
|
+
def access_token_password
|
34
|
+
access_token && access_token['password']
|
35
|
+
end
|
11
36
|
|
12
37
|
class << self
|
13
38
|
def default_filename
|
14
39
|
'config.yml'
|
15
40
|
end
|
41
|
+
|
42
|
+
def exists?(path)
|
43
|
+
File.exist?(full_path(path))
|
44
|
+
end
|
45
|
+
|
46
|
+
def init(path, filename = nil)
|
47
|
+
FileUtils.mkdir_p(default_dir)
|
48
|
+
FileUtils.mkdir_p(drivers_dir)
|
49
|
+
|
50
|
+
File.open(full_path(path, filename), 'w+') do |file|
|
51
|
+
file.write({}.to_yaml)
|
52
|
+
end
|
53
|
+
|
54
|
+
new(path, filename)
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_dir
|
58
|
+
File.expand_path("~/.mode")
|
59
|
+
end
|
60
|
+
|
61
|
+
def drivers_dir
|
62
|
+
File.join(default_dir, 'drivers')
|
63
|
+
end
|
64
|
+
|
65
|
+
def full_path(path, filename = nil)
|
66
|
+
File.expand_path(File.join(path, filename || default_filename))
|
67
|
+
end
|
16
68
|
end
|
17
69
|
|
18
70
|
private
|
@@ -21,13 +73,19 @@ module Mode
|
|
21
73
|
@username = config['username']
|
22
74
|
@access_token = config['access_token']
|
23
75
|
@environment = config['environment'].to_s
|
76
|
+
|
77
|
+
@data_sources ||= []
|
78
|
+
(config['data_sources'] || []).each do |name, props|
|
79
|
+
data_sources << Mode::Connector::DataSource.new(name, props)
|
80
|
+
end
|
24
81
|
end
|
25
82
|
|
26
83
|
def to_yaml
|
27
84
|
{
|
28
85
|
'username' => username,
|
29
86
|
'access_token' => access_token,
|
30
|
-
'environment' => environment
|
87
|
+
'environment' => environment,
|
88
|
+
'data_sources' => data_sources
|
31
89
|
}.to_yaml
|
32
90
|
end
|
33
91
|
end
|
@@ -1,26 +1,68 @@
|
|
1
|
-
require 'daemon_spawn'
|
2
|
-
|
3
1
|
module Mode
|
4
2
|
module Connector
|
5
|
-
class Daemon
|
3
|
+
class Daemon
|
4
|
+
attr_reader :max_jobs
|
6
5
|
attr_reader :scheduler
|
7
6
|
|
8
|
-
def
|
9
|
-
max_jobs =
|
10
|
-
|
11
|
-
|
12
|
-
@scheduler = Mode::Connector::Scheduler.new(
|
13
|
-
data_sources, :max_jobs => max_jobs
|
14
|
-
)
|
7
|
+
def initialize(options = {})
|
8
|
+
@max_jobs = options[:max_jobs].to_i || 4
|
9
|
+
end
|
15
10
|
|
16
|
-
|
11
|
+
def start
|
12
|
+
load_drivers
|
13
|
+
configure_api
|
14
|
+
scheduler.start!
|
17
15
|
rescue => err
|
18
16
|
Mode::Logger.instance.error(
|
19
|
-
"Connector::
|
17
|
+
"Connector::Runner", err.message, err.backtrace)
|
20
18
|
end
|
21
19
|
|
22
20
|
def stop
|
23
21
|
scheduler.stop!
|
22
|
+
rescue => err
|
23
|
+
Mode::Logger.instance.error(
|
24
|
+
"Connector::Runner", err.message, err.backtrace)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def scheduler
|
30
|
+
@scheduler ||= Mode::Connector::Scheduler.new(
|
31
|
+
data_sources, :max_jobs => max_jobs
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_drivers
|
36
|
+
Dir[File.join(drivers_dir, '*.jar')].each do |driver|
|
37
|
+
load driver
|
38
|
+
end if RUBY_PLATFORM == 'java'
|
39
|
+
end
|
40
|
+
|
41
|
+
def configure_api
|
42
|
+
Mode::API::Request.configure(
|
43
|
+
config.environment, {
|
44
|
+
'credentials' => {
|
45
|
+
'username' => config.username,
|
46
|
+
'password' => config.access_token_password
|
47
|
+
}, 'access_token' => config.access_token
|
48
|
+
}
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def data_sources
|
53
|
+
config.data_sources
|
54
|
+
end
|
55
|
+
|
56
|
+
def config_dir
|
57
|
+
Mode::Config.default_dir
|
58
|
+
end
|
59
|
+
|
60
|
+
def drivers_dir
|
61
|
+
File.join(config_dir, 'drivers')
|
62
|
+
end
|
63
|
+
|
64
|
+
def config
|
65
|
+
@config ||= Mode::Config.new(config_dir)
|
24
66
|
end
|
25
67
|
end
|
26
68
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'spoon'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Mode
|
5
|
+
module Connector
|
6
|
+
class Daemonizer
|
7
|
+
attr_reader :args
|
8
|
+
attr_reader :timeout
|
9
|
+
attr_reader :pid_file
|
10
|
+
attr_reader :executable
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@args = options[:args] || []
|
14
|
+
@timeout = options[:timeout] || 10
|
15
|
+
@pid_file = options[:pid_file] || default_pid_file
|
16
|
+
@executable = options[:executable] || default_executable
|
17
|
+
end
|
18
|
+
|
19
|
+
def alive?
|
20
|
+
return false unless pid
|
21
|
+
Process.kill(0, pid)
|
22
|
+
true
|
23
|
+
rescue Errno::ESRCH, TypeError # PID is NOT running or is zombied
|
24
|
+
false
|
25
|
+
rescue Errno::EPERM
|
26
|
+
STDERR.puts "Permission denied for process #{pid}!";
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def restart
|
31
|
+
begin
|
32
|
+
stop
|
33
|
+
ensure
|
34
|
+
start
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def start
|
39
|
+
abort "Mode connector already running" if alive?
|
40
|
+
abort "Insufficient permissions to start Mode connector" unless File.executable?(executable)
|
41
|
+
|
42
|
+
if pid = Spoon.spawnp(executable, *args.collect(&:to_s))
|
43
|
+
create_pid(pid)
|
44
|
+
STDOUT.puts "Mode connector running with pid #{pid}"
|
45
|
+
else
|
46
|
+
Process.setsid
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def stop
|
51
|
+
abort "Mode connector not running, nothing to stop" unless alive?
|
52
|
+
|
53
|
+
begin
|
54
|
+
STDOUT.puts "Stopping process #{pid}..."
|
55
|
+
|
56
|
+
Timeout.timeout(timeout) do
|
57
|
+
Process.kill("TERM", pid)
|
58
|
+
sleep 1 while alive?
|
59
|
+
end
|
60
|
+
rescue Timeout::Error
|
61
|
+
STDERR.puts "Graceful shutdown timeout, sending KILL signal"
|
62
|
+
Process.kill("KILL", pid)
|
63
|
+
sleep 0.1 while alive?
|
64
|
+
end
|
65
|
+
|
66
|
+
remove_pid
|
67
|
+
STDOUT.puts "Mode connector stopped."
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def pid
|
73
|
+
@pid ||= File.read(pid_file).gsub(/[^0-9]/,'').to_i
|
74
|
+
rescue Errno::ENOENT => e
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def remove_pid
|
79
|
+
File.unlink(pid_file)
|
80
|
+
rescue => e
|
81
|
+
STDERR.puts "ERROR: Unable to unlink #{pid_file}:\n\t" +
|
82
|
+
"(#{e.class}) #{e.message}"
|
83
|
+
exit
|
84
|
+
end
|
85
|
+
|
86
|
+
def create_pid(pid)
|
87
|
+
File.open(pid_file, 'w') { |f| f.puts pid }
|
88
|
+
rescue => e
|
89
|
+
STDERR.puts "Error: Unable to open #{pid_file} for writing:\n\t" +
|
90
|
+
"(#{e.class}) #{e.message}"
|
91
|
+
exit!
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_pid_file
|
95
|
+
File.join(Mode::Config.default_dir, 'connect.pid')
|
96
|
+
end
|
97
|
+
|
98
|
+
def default_lib_path
|
99
|
+
File.join(File.dirname(__FILE__), '..', '..')
|
100
|
+
end
|
101
|
+
|
102
|
+
def default_executable
|
103
|
+
File.expand_path(File.join(default_lib_path, 'connect.rb'))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -43,21 +43,28 @@ module Mode
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def select(query, &block)
|
46
|
-
|
46
|
+
log_connection_query(query)
|
47
|
+
connection.dataset.fetch_rows(query, &block)
|
47
48
|
end
|
48
49
|
|
49
50
|
def connection
|
50
|
-
@connection ||= Sequel.connect(connection_url, adapter_opts)
|
51
|
+
@connection ||= Sequel.connect(connection_url, adapter_opts).tap do |conn|
|
52
|
+
conn.extension(:connection_validator)
|
53
|
+
end
|
51
54
|
end
|
52
55
|
|
53
|
-
def
|
54
|
-
adapter
|
56
|
+
def jdbc?
|
57
|
+
adapter.start_with?('jdbc')
|
55
58
|
end
|
56
59
|
|
57
60
|
def postgres?
|
58
61
|
adapter == 'postgres'
|
59
62
|
end
|
60
63
|
|
64
|
+
def redshift?
|
65
|
+
['redshift', 'jdbc:redshift'].include?(adapter)
|
66
|
+
end
|
67
|
+
|
61
68
|
private
|
62
69
|
|
63
70
|
def adapter_opts
|
@@ -77,25 +84,45 @@ module Mode
|
|
77
84
|
opts
|
78
85
|
end
|
79
86
|
|
87
|
+
def adapter_segment
|
88
|
+
case adapter
|
89
|
+
when 'jdbc:redshift'
|
90
|
+
'jdbc:postgresql'
|
91
|
+
else
|
92
|
+
adapter
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
80
96
|
def port_segment
|
81
97
|
port.nil? ? nil : ":#{port}"
|
82
98
|
end
|
83
99
|
|
84
100
|
def password_segment
|
101
|
+
jdbc? ? jdbc_password_segment : standard_password_segment
|
102
|
+
end
|
103
|
+
|
104
|
+
def jdbc_password_segment
|
105
|
+
password.nil? ? nil : "&password=#{password}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def standard_password_segment
|
85
109
|
password.nil? ? nil : ":#{password}"
|
86
110
|
end
|
87
111
|
|
88
112
|
def connection_url
|
89
|
-
|
113
|
+
jdbc? ? jdbc_connection_url : standard_connection_url
|
90
114
|
end
|
91
115
|
|
92
|
-
def
|
93
|
-
|
94
|
-
|
95
|
-
|
116
|
+
def jdbc_connection_url
|
117
|
+
"#{adapter_segment}://#{host}#{port_segment}/#{database}?user=#{username}#{password_segment}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def standard_connection_url
|
121
|
+
"#{adapter_segment}://#{username}#{password_segment}@#{host}#{port_segment}/#{database}"
|
96
122
|
end
|
97
123
|
|
98
124
|
def log_connection_query(query)
|
125
|
+
return if query.nil?
|
99
126
|
Mode::Logger.instance.debug(
|
100
127
|
"Connect::DataSource", "QUERY", query.split("\n"))
|
101
128
|
end
|