mode 0.0.10 → 0.0.11

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