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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -0
  3. data/.travis.yml +1 -0
  4. data/bin/mode +3 -8
  5. data/lib/connect.rb +13 -0
  6. data/lib/mode.rb +18 -5
  7. data/lib/mode/api/link.rb +4 -0
  8. data/lib/mode/api/request.rb +20 -10
  9. data/lib/mode/auth/access_token.rb +16 -0
  10. data/lib/mode/cli/connect.rb +1 -1
  11. data/lib/mode/commands/analyze_field.rb +1 -3
  12. data/lib/mode/commands/connect.rb +24 -49
  13. data/lib/mode/commands/helpers.rb +23 -18
  14. data/lib/mode/commands/import.rb +2 -4
  15. data/lib/mode/commands/login.rb +88 -25
  16. data/lib/mode/config.rb +60 -2
  17. data/lib/mode/connector/connect.rb +11 -0
  18. data/lib/mode/connector/daemon.rb +54 -12
  19. data/lib/mode/connector/daemonizer.rb +107 -0
  20. data/lib/mode/connector/data_source.rb +36 -9
  21. data/lib/mode/connector/scheduler.rb +6 -5
  22. data/lib/mode/connector/uploader.rb +3 -4
  23. data/lib/mode/version.rb +1 -1
  24. data/mode.gemspec +7 -4
  25. data/spec/api/request_spec.rb +9 -6
  26. data/spec/commands/connect_spec.rb +45 -59
  27. data/spec/commands/helpers_spec.rb +9 -22
  28. data/spec/commands/import_spec.rb +5 -0
  29. data/spec/commands/login_spec.rb +87 -76
  30. data/spec/config_spec.rb +9 -0
  31. data/spec/connector/daemon_spec.rb +11 -6
  32. data/spec/connector/daemonizer_spec.rb +106 -0
  33. data/spec/connector/data_source_spec.rb +15 -17
  34. data/spec/connector/registrar_spec.rb +5 -6
  35. data/spec/connector/scheduler_spec.rb +3 -2
  36. data/spec/connector/uploader_spec.rb +9 -9
  37. metadata +65 -65
  38. data/lib/mode/configurable.rb +0 -46
  39. data/lib/mode/connector/config.rb +0 -31
  40. data/spec/connector/config_spec.rb +0 -46
@@ -23,8 +23,8 @@ module Mode
23
23
 
24
24
  def execute
25
25
  username, password = configure_credentials
26
- chosen_token = choose_access_token.token
27
- update_configuration(environment, username, chosen_token)
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
- :credentials => {
40
- :username => username,
41
- :access_token => password
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 fetch_access_tokens
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.embedded('access_tokens').map do |access_token|
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 prompt_access_tokens(access_tokens)
76
- say "\nPlease choose an access token to use:\n\n"
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
- choices = []
79
- access_tokens.each_with_index do |token, index|
80
- choices << (index + 1).to_s
81
- say "#{index + 1}. #{token.name}"
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.nil?
90
- raise "Couldn't retrieve access tokens"
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
- chosen = prompt_access_tokens(access_tokens)
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}"
@@ -2,17 +2,69 @@ require 'yaml'
2
2
 
3
3
  module Mode
4
4
  class Config
5
- include Mode::Configurable
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
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'mode'
4
+
5
+ runner = Mode::Connector::Runner.new(:max_jobs => ARGV[0] || 4)
6
+
7
+ trap("INT") { runner.stop }
8
+ trap("HUP") { runner.stop }
9
+ trap("TERM") { runner.stop }
10
+
11
+ runner.start
@@ -1,26 +1,68 @@
1
- require 'daemon_spawn'
2
-
3
1
  module Mode
4
2
  module Connector
5
- class Daemon < DaemonSpawn::Base
3
+ class Daemon
4
+ attr_reader :max_jobs
6
5
  attr_reader :scheduler
7
6
 
8
- def start(args)
9
- max_jobs = args.shift
10
- data_sources = args.shift
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
- @scheduler.start!
11
+ def start
12
+ load_drivers
13
+ configure_api
14
+ scheduler.start!
17
15
  rescue => err
18
16
  Mode::Logger.instance.error(
19
- "Connector::Daemon", err.message, err.backtrace)
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
- connection_dataset(query).each(&block)
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 redshift?
54
- adapter == 'redshift'
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
- "#{adapter}://#{username}#{password_segment}@#{host}#{port_segment}/#{database}"
113
+ jdbc? ? jdbc_connection_url : standard_connection_url
90
114
  end
91
115
 
92
- def connection_dataset(query)
93
- log_connection_query(query)
94
- connection.dataset.with_sql(query)
95
- # figure out how to make cursors work here
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