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