td 0.14.1 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0fd96802c069458c8a45adde2c63044dbc6157e5
4
- data.tar.gz: 4ca331507c1ec04cac6ad1519cee73aa86f5e33a
3
+ metadata.gz: 61447ffe419ff8f382b433975388f420a4b7d013
4
+ data.tar.gz: fedca79cdca59a6a91eb4968ca3d5508e3bf65b8
5
5
  SHA512:
6
- metadata.gz: 9a8ffee2dc536d83831e9c7a6f8e092273d2434ab5b401dedfadd756e782a4c20b2655d44478d69cbbe15bf8167c696ac605aed84bddf0efd75676f6b60ff59a
7
- data.tar.gz: efc8acd54a2df5304f6f55186fa44717d3427c4820c5bf964a8e88a05f2897980d5c2e121591de3feba47d1fb4766e7061bff9ec34f43eae10a09d8c9c7d7fac
6
+ metadata.gz: 0d85fb2e56cb54143505b472a18ff26a9b532db3ddf0a7033d7f8610626f564c9ec90a9dfece2cff4dee5447945d82cf481a061fc6eb88a43826205d7fc0c01e
7
+ data.tar.gz: ef7d72a9d251c546098ac39b2a3aa455dc933dfe224286ed4569b49f443f054c3acdb8ed1672738902b51731092354de11a1d8300f1690c3081cb89823cd50fc
@@ -1,5 +1,8 @@
1
1
  language: ruby
2
2
 
3
+ jdk:
4
+ - oraclejdk8
5
+
3
6
  rvm:
4
7
  - 1.9.3
5
8
  - 2.0.0
data/ChangeLog CHANGED
@@ -1,3 +1,8 @@
1
+ == 2016-09-06 version 0.15.0
2
+
3
+ * Add `td workflows` command. #167
4
+ * Add `td connector:update` options to allow configuring several additional data connector settings. #127 #155
5
+
1
6
  == 2016-07-11 version 0.14.1
2
7
 
3
8
  * Use td-client-ruby v0.8.82
@@ -14,3 +14,4 @@ test_script:
14
14
  environment:
15
15
  matrix:
16
16
  - ruby_version: "22"
17
+ - ruby_version: "22-x64"
@@ -81,6 +81,10 @@ subcommands=(
81
81
  "table\:tail":"Get recently imported logs"
82
82
  "table\:partial_delete":"Delete logs from the table within the specified time range"
83
83
 
84
+ "wf":"Run a workflow command"
85
+ "workflow":"Run a workflow command"
86
+ "workflow:reset":"Reset the workflow module"
87
+
84
88
  # TODO: Add ACL related commands
85
89
  )
86
90
 
@@ -41,7 +41,7 @@ module Command
41
41
  end
42
42
  end
43
43
 
44
- $stdout.puts "Enter your Treasure Data credentials."
44
+ $stdout.puts "Enter your Treasure Data credentials. For Google SSO user, please see https://docs.treasuredata.com/articles/command-line#google-sso-users"
45
45
  unless user_name
46
46
  begin
47
47
  $stdout.print "Email: "
@@ -25,6 +25,9 @@ module Command
25
25
  class ImportError < RuntimeError
26
26
  end
27
27
 
28
+ class WorkflowError < RuntimeError
29
+ end
30
+
28
31
  private
29
32
  def initialize
30
33
  @render_indent = ''
@@ -212,12 +212,47 @@ module Command
212
212
  end
213
213
 
214
214
  def connector_update(op)
215
- name, config_file = op.cmd_parse
215
+ settings = {}
216
216
 
217
- config = prepare_bulkload_job_config(config_file)
217
+ op.on('-n', '--newname NAME', 'change the schedule\'s name', String) {|n|
218
+ settings['name'] = n
219
+ }
220
+ op.on('-d', '--database DB_NAME', 'change the database', String) {|s|
221
+ settings['database'] = s
222
+ }
223
+ op.on('-t', '--table TABLE_NAME', 'change the table', String) {|s|
224
+ settings['table'] = s
225
+ }
226
+ op.on('-s', '--schedule [CRON]', 'change the schedule or leave blank to remove the schedule', String) {|s|
227
+ settings['cron'] = s || ''
228
+ }
229
+ op.on('-z', '--timezone TZ', "name of the timezone.",
230
+ " Only extended timezones like 'Asia/Tokyo', 'America/Los_Angeles' are supported,",
231
+ " (no 'PST', 'PDT', etc...).",
232
+ " When a timezone is specified, the cron schedule is referred to that timezone.",
233
+ " Otherwise, the cron schedule is referred to the UTC timezone.",
234
+ " E.g. cron schedule '0 12 * * *' will execute daily at 5 AM without timezone option",
235
+ " and at 12PM with the -t / --timezone 'America/Los_Angeles' timezone option", String) {|s|
236
+ settings['timezone'] = s
237
+ }
238
+ op.on('-D', '--delay SECONDS', 'change the delay time of the schedule', Integer) {|i|
239
+ settings['delay'] = i
240
+ }
241
+ op.on('-T', '--time-column COLUMN_NAME', 'change the name of the time column', String) {|s|
242
+ settings['time_column'] = s
243
+ }
244
+ op.on('-c', '--config CONFIG_FILE', 'update the connector configuration', String) {|s|
245
+ settings['config'] = s
246
+ }
247
+ op.on('--config-diff CONFIG_DIFF_FILE', "update the connector config_diff", String) { |s| settings['config_diff'] = s }
218
248
 
249
+ name, config_file = op.cmd_parse
250
+ settings['config'] = config_file if config_file
251
+ op.cmd_usage 'nothing to update' if settings.empty?
252
+ settings['config'] = prepare_bulkload_job_config(settings['config']) if settings.key?('config')
253
+ settings['config_diff'] = prepare_bulkload_job_config(settings['config_diff']) if settings.key?('config_diff')
219
254
  client = get_client()
220
- session = client.bulk_load_update(name, config: config)
255
+ session = client.bulk_load_update(name, settings)
221
256
  dump_connector_session(session)
222
257
  end
223
258
 
@@ -312,6 +347,11 @@ private
312
347
 
313
348
 
314
349
  def prepare_bulkload_job_config(config_file)
350
+ config = prepare_bulkload_job_config_diff(config_file)
351
+ TreasureData::ConnectorConfigNormalizer.new(config).normalized_config
352
+ end
353
+
354
+ def prepare_bulkload_job_config_diff(config_file)
315
355
  unless File.exist?(config_file)
316
356
  raise ParameterConfigurationError, "configuration file: #{config_file} not found"
317
357
  end
@@ -326,8 +366,7 @@ private
326
366
  rescue => e
327
367
  raise ParameterConfigurationError, "configuration file: #{config_file} #{e.message}"
328
368
  end
329
-
330
- TreasureData::ConnectorConfigNormalizer.new(config).normalized_config
369
+ config
331
370
  end
332
371
 
333
372
  def dump_connector_session(session)
@@ -352,11 +352,15 @@ module List
352
352
  add_list 'connector:list', %w[], 'Show list of connector sessions', ['connector:list']
353
353
  add_list 'connector:create', %w[name cron database table config], 'Create new connector session', ['connector:create connector1 "0 * * * *" connector_database connector_table td-bulkload.yml']
354
354
  add_list 'connector:show', %w[name], 'Show connector session', ['connector:show connector1']
355
- add_list 'connector:update', %w[name config], 'Modify connector session', ['connector:update connector1 td-bulkload.yml']
355
+ add_list 'connector:update', %w[name config?], 'Modify connector session', ['connector:update connector1 -c td-bulkload.yml -s \'@daily\' ...']
356
356
  add_list 'connector:delete', %w[name], 'Delete connector session', ['connector:delete connector1']
357
357
  add_list 'connector:history', %w[name], 'Show job history of connector session', ['connector:history connector1']
358
358
  add_list 'connector:run', %w[name time?], 'Run connector with session for the specified time option', ['connector:run connector1 "2016-01-01 00:00:00"']
359
359
 
360
+ add_list 'workflow', %w[], 'Run a workflow command'
361
+ add_list 'workflow:reset', %w[], 'Reset the workflow module'
362
+ add_list 'workflow:version', %w[], 'Show workflow module version'
363
+
360
364
  # aliases
361
365
  add_alias 'db', 'db:show'
362
366
  add_alias 'dbs', 'db:list'
@@ -417,6 +421,8 @@ module List
417
421
 
418
422
  add_alias 'connector', 'connector:guess'
419
423
 
424
+ add_alias 'wf', 'workflow'
425
+
420
426
  # backward compatibility
421
427
  add_alias 'show-databases', 'db:list'
422
428
  add_alias 'show-dbs', 'db:list'
@@ -47,6 +47,7 @@ Basic commands:
47
47
  sched # create/delete/list schedules that run a query periodically
48
48
  schema # create/delete/modify schemas of tables
49
49
  connector # manage connectors
50
+ workflow # manage workflows
50
51
 
51
52
  Additional commands:
52
53
 
@@ -162,12 +163,13 @@ EOF
162
163
  return 1
163
164
  end
164
165
 
166
+ status = nil
165
167
  begin
166
168
  # test the connectivity with the API endpoint
167
169
  if cmd_req_connectivity && Config.cl_endpoint
168
170
  Command.test_api_endpoint(Config.endpoint)
169
171
  end
170
- method.call(argv)
172
+ status = method.call(argv)
171
173
  rescue ConfigError
172
174
  $stderr.puts "TreasureData account is not configured yet."
173
175
  $stderr.puts "Run '#{$prog} account' first."
@@ -185,7 +187,7 @@ EOF
185
187
  # => NotFoundError
186
188
  # => AuthError
187
189
  if ![ParameterConfigurationError, BulkImportExecutionError, UpdateError, ImportError,
188
- APIError, ForbiddenError, NotFoundError, AuthError, AlreadyExistsError].include?(e.class) ||
190
+ APIError, ForbiddenError, NotFoundError, AuthError, AlreadyExistsError, WorkflowError].include?(e.class) ||
189
191
  !ENV['TD_TOOLBELT_DEBUG'].nil? || $verbose
190
192
  show_backtrace "Error #{$!.class}: backtrace:", $!.backtrace
191
193
  end
@@ -211,7 +213,7 @@ EOS
211
213
  end
212
214
  return 1
213
215
  end
214
- return 0
216
+ return (status.is_a? Integer) ? status : 0
215
217
  end
216
218
 
217
219
  private
@@ -0,0 +1,446 @@
1
+ require 'td/helpers'
2
+ require 'td/updater'
3
+ require 'open3'
4
+ require 'pathname'
5
+ require 'time'
6
+ require 'yaml'
7
+
8
+ module TreasureData
9
+ module Command
10
+ include TreasureData::Helpers
11
+
12
+ # The workflow entrypoint command. Invokes the digdag cli, passing on any command line arguments.
13
+ def workflow(op, capture_output=false, check_prereqs=true)
14
+ if Config.apikey.nil?
15
+ raise ConfigError
16
+ end
17
+ check_digdag_cli if check_prereqs
18
+ cmd = [
19
+ java_cmd,
20
+ '-Dio.digdag.cli.programName=td workflow',
21
+ '-XX:+TieredCompilation', '-XX:TieredStopAtLevel=1', '-Xverify:none'
22
+ ]
23
+
24
+ FileUtils.mkdir_p digdag_tmp_dir
25
+ Dir.mktmpdir(nil, digdag_tmp_dir) { |wd|
26
+ env = {}
27
+ digdag_config_path = File.join(wd, 'config')
28
+ FileUtils.touch(digdag_config_path)
29
+ if Config.cl_apikey
30
+ # If the user passes the apikey on the command line we cannot use the digdag td.conf plugin.
31
+ # Instead, create a digdag configuration file with the endpoint and the specified apikey.
32
+ env['TD_CONFIG_PATH'] = nil
33
+ apikey = TreasureData::Config.apikey
34
+ File.write(digdag_config_path, [
35
+ 'client.http.endpoint = https://api-workflow.treasuredata.com',
36
+ "client.http.headers.authorization = TD1 #{apikey}",
37
+ "secrets.td.apikey = #{apikey}"
38
+ ].join($/) + $/)
39
+ cmd << '-Dio.digdag.standards.td.secrets.enabled=false'
40
+ else
41
+ # Use the digdag td.conf plugin to configure wf api and apikey.
42
+ env['TREASURE_DATA_CONFIG_PATH'] = Config.path
43
+ cmd << '-Dio.digdag.standards.td.client-configurator.enabled=true'
44
+ end
45
+
46
+ cmd << '-jar' << digdag_cli_path
47
+ unless op.argv.empty?
48
+ cmd << '--config' << digdag_config_path
49
+ end
50
+ cmd.concat(op.argv)
51
+
52
+ unless ENV['TD_TOOLBELT_DEBUG'].nil?
53
+ $stderr.puts cmd.to_s
54
+ end
55
+
56
+ if capture_output
57
+ # TODO: use popen3 instead?
58
+ stdout_str, stderr_str, status = Open3.capture3(env, *cmd)
59
+ $stdout.write(stdout_str)
60
+ $stderr.write(stderr_str)
61
+ return status.exitstatus
62
+ else
63
+ Kernel::system(env, *cmd)
64
+ return $?.exitstatus
65
+ end
66
+ }
67
+ end
68
+
69
+ # "Factory reset"
70
+ def workflow_reset(op)
71
+ $stdout << 'Removing workflow module...'
72
+ FileUtils.rm_rf digdag_dir
73
+ $stdout.puts ' Done.'
74
+ return 0
75
+ end
76
+
77
+ def workflow_version(op)
78
+ unless File.exists?(digdag_cli_path)
79
+ $stderr.puts('Workflow module not yet installed.')
80
+ return 1
81
+ end
82
+
83
+ $stdout.puts("Bundled Java: #{bundled_java?}")
84
+
85
+ begin
86
+ out, status = Open3.capture2e(java_cmd, '-version')
87
+ raise unless status.success?
88
+ rescue
89
+ $stderr.puts('Failed to run java')
90
+ return 1
91
+ end
92
+ $stdout.puts(out)
93
+
94
+ version_op = List::CommandParser.new("workflow", [], [], nil, ['--version'], true)
95
+ $stdout.write('Digdag version: ')
96
+ workflow(version_op, capture_output=true, check_prereqs=false)
97
+ end
98
+
99
+ private
100
+ def system_java_cmd
101
+ if td_wf_java.nil? or td_wf_java.empty?
102
+ 'java'
103
+ else
104
+ td_wf_java
105
+ end
106
+ end
107
+
108
+ private
109
+ def bundled_java?
110
+ if not td_wf_java.empty?
111
+ return false
112
+ end
113
+ return Helpers.on_64bit_os?
114
+ end
115
+
116
+ private
117
+ def td_wf_java
118
+ ENV.fetch('TD_WF_JAVA', '').strip
119
+ end
120
+
121
+ def digdag_base_url
122
+ 'http://toolbelt.treasure-data.com/digdag'
123
+ end
124
+
125
+ private
126
+ def digdag_url
127
+ url = ENV.fetch('TD_DIGDAG_URL', '').strip
128
+ return url unless url.empty?
129
+ user = Config.read['account.user']
130
+ if user.nil? or user.strip.empty?
131
+ raise ConfigError
132
+ end
133
+ query = URI.encode_www_form('user' => user)
134
+ "#{digdag_base_url}?#{query}"
135
+ end
136
+
137
+ private
138
+ def digdag_dir
139
+ File.join(home_directory, '.td', 'digdag')
140
+ end
141
+
142
+ private
143
+ def digdag_tmp_dir
144
+ File.join(home_directory, '.td', 'digdag', 'tmp')
145
+ end
146
+
147
+ private
148
+ def digdag_cli_path
149
+ File.join(digdag_dir, 'digdag')
150
+ end
151
+
152
+ private
153
+ def digdag_jre_dir
154
+ File.join(digdag_dir, 'jre')
155
+ end
156
+
157
+ private
158
+ def digdag_jre_tmp_dir
159
+ File.join(digdag_dir, 'jre.tmp')
160
+ end
161
+
162
+ private
163
+ def java_cmd
164
+ if bundled_java?
165
+ digdag_java_path
166
+ else
167
+ system_java_cmd
168
+ end
169
+ end
170
+
171
+ private
172
+ def digdag_java_path
173
+ File.join(digdag_jre_dir, 'bin', 'java')
174
+ end
175
+
176
+ private
177
+ def digdag_cli_tmp_path
178
+ File.join(digdag_dir, 'digdag.tmp')
179
+ end
180
+
181
+ private
182
+ def jre_archive
183
+ # XXX (dano): platform detection could be more robust
184
+ if Helpers.on_64bit_os?
185
+ if Helpers.on_windows?
186
+ return 'win_x64'
187
+ elsif Helpers.on_mac?
188
+ return 'mac_x64'
189
+ else # Assume linux
190
+ return 'lin_x64'
191
+ end
192
+ end
193
+ raise 'OS architecture not supported'
194
+ end
195
+
196
+ private
197
+ def jre_url
198
+ base_url = ENV.fetch('TD_WF_JRE_BASE_URL', 'http://toolbelt.treasuredata.com/digdag/jdk/')
199
+ "#{base_url}#{jre_archive}"
200
+ end
201
+
202
+ private
203
+ def fail_system_java
204
+ raise WorkflowError, <<EOF
205
+ A suitable installed version of Java could not be found and and Java cannot be
206
+ automatically installed for this OS.
207
+
208
+ Please install at least Java 8u71.
209
+ EOF
210
+ end
211
+
212
+ private
213
+ def detect_system_java
214
+ begin
215
+ output, status = Open3.capture2e(system_java_cmd, '-version')
216
+ rescue => e
217
+ return false
218
+ end
219
+ unless status.success?
220
+ return false
221
+ end
222
+ if output =~ /openjdk version/ or output =~ /java version/
223
+ m = output.match(/version "(\d+)\.(\d+)\.(\d+)(?:_(\d+))"/)
224
+ if not m or m.size < 4
225
+ return false
226
+ end
227
+ # Check for at least Java 8. Let digdag itself verify revision.
228
+ major = m[1].to_i
229
+ minor = m[2].to_i
230
+ if major < 1 or minor < 8
231
+ return false
232
+ end
233
+ end
234
+ return true
235
+ end
236
+
237
+ private
238
+ def check_system_java
239
+ # Trust the user if they've specified a jre to use
240
+ if td_wf_java.empty?
241
+ unless detect_system_java
242
+ fail_system_java
243
+ end
244
+ end
245
+ end
246
+
247
+ # Follow all redirects and return the resulting url
248
+ def resolve_url(url)
249
+ require 'net/http'
250
+ require 'openssl'
251
+
252
+ uri = URI(url)
253
+ http_class = Command.get_http_class
254
+ http = http_class.new(uri.host, uri.port)
255
+
256
+ if uri.scheme == 'https'
257
+ http.use_ssl = true
258
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
259
+ end
260
+
261
+ http.request_get(uri.path + (uri.query ? '?' + uri.query : '')) {|response|
262
+ if response.class == Net::HTTPOK
263
+ return url
264
+ elsif response.class == Net::HTTPFound || \
265
+ response.class == Net::HTTPRedirection
266
+ unless ENV['TD_TOOLBELT_DEBUG'].nil?
267
+ $stdout.puts "redirect '#{url}' to '#{response['Location']}'... "
268
+ end
269
+ return resolve_url(response['Location'])
270
+ else
271
+ raise_error "An error occurred when fetching from '#{uri}' " +
272
+ "(#{response.class.to_s}: #{response.message})."
273
+ return false
274
+ end
275
+ }
276
+ end
277
+
278
+ private
279
+ def download_java
280
+ if File.exists?(digdag_jre_dir)
281
+ return
282
+ end
283
+
284
+ require 'net/http'
285
+ require 'openssl'
286
+
287
+ Dir.mktmpdir do |download_dir|
288
+ indicator = Command::TimeBasedDownloadProgressIndicator.new(
289
+ 'Downloading Java...', Time.new.to_i, 2)
290
+ status = nil
291
+ real_jre_uri = URI(resolve_url(jre_url))
292
+ jre_filename = Pathname.new(real_jre_uri.path).basename.to_s
293
+ download_path = File.join(download_dir, jre_filename)
294
+ File.open(download_path, 'wb') do |file|
295
+ status = Updater.stream_fetch(jre_url, file) {
296
+ indicator.update
297
+ }
298
+ end
299
+ indicator.finish
300
+
301
+ $stdout.puts
302
+
303
+ unless status
304
+ raise WorkflowError, 'Failed to download Java.'
305
+ end
306
+
307
+ $stdout.print 'Installing Java... '
308
+ FileUtils.rm_rf digdag_jre_tmp_dir
309
+ FileUtils.mkdir_p digdag_jre_tmp_dir
310
+ extract_archive(download_path, digdag_jre_tmp_dir, 1)
311
+ FileUtils.mv digdag_jre_tmp_dir, digdag_jre_dir
312
+ $stdout.puts 'done'
313
+ end
314
+ end
315
+
316
+ private
317
+ def extract_archive(archive, destination, strip)
318
+ if archive.end_with? '.tar.gz'
319
+ extract_tarball(archive, destination, strip)
320
+ elsif archive.end_with? '.zip'
321
+ extract_zip(archive, destination, strip)
322
+ end
323
+ end
324
+
325
+ private
326
+ def extract_zip(zip_archive, destination, strip)
327
+ require 'fileutils'
328
+ require 'zip/zip'
329
+ Zip::ZipFile.open(zip_archive) { |zip_file|
330
+ zip_file.each { |f|
331
+ stripped = strip_components(f.name, strip)
332
+ if stripped.empty?
333
+ next
334
+ end
335
+ dest = File.join destination, stripped
336
+ FileUtils.rm_rf dest if File.exist? dest
337
+ FileUtils.mkdir_p(File.dirname(dest))
338
+ zip_file.extract(f, dest)
339
+ }
340
+ }
341
+ end
342
+
343
+ # http://stackoverflow.com/a/31310593
344
+ TAR_LONGLINK = '././@LongLink'
345
+ private
346
+ def extract_tarball(tar_gz_archive, destination, strip)
347
+ require 'fileutils'
348
+ require 'rubygems/package'
349
+ require 'zlib'
350
+
351
+ Zlib::GzipReader.open tar_gz_archive do |gzip_reader|
352
+ Gem::Package::TarReader.new(gzip_reader) do |tar|
353
+ filename = nil
354
+ tar.each do |entry|
355
+ # Handle LongLink
356
+ if entry.full_name == TAR_LONGLINK
357
+ filename = entry.read.strip
358
+ next
359
+ end
360
+ filename ||= entry.full_name
361
+
362
+ # Strip path components
363
+ stripped = strip_components(filename, strip)
364
+ filename = nil
365
+ if stripped.empty?
366
+ next
367
+ end
368
+ dest = File.join destination, stripped
369
+
370
+ if entry.directory? || (entry.header.typeflag == '' && entry.full_name.end_with?('/'))
371
+ File.rm_rf dest if File.file? dest
372
+ FileUtils.mkdir_p dest, :mode => entry.header.mode, :verbose => false
373
+ elsif entry.file? || (entry.header.typeflag == '' && !entry.full_name.end_with?('/'))
374
+ FileUtils.rm_rf dest if File.exist? dest
375
+ FileUtils.mkdir_p File.dirname dest
376
+ File.open dest, "wb" do |f|
377
+ f.print entry.read
378
+ end
379
+ FileUtils.chmod entry.header.mode, dest, :verbose => false
380
+ elsif entry.header.typeflag == '2' #Symlink!
381
+ File.symlink entry.header.linkname, dest
382
+ else
383
+ raise "Unkown tar entry: #{entry.full_name} type: #{entry.header.typeflag}."
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end
389
+
390
+ def strip_components(filename, strip)
391
+ File.join(Pathname.new(filename).each_filename.drop(strip))
392
+ end
393
+
394
+ private
395
+ def check_digdag_cli
396
+ check_system_java unless bundled_java?
397
+
398
+ unless File.exists?(digdag_cli_path)
399
+ $stderr.puts 'Workflow module not yet installed, download now? [Y/n]'
400
+ line = $stdin.gets
401
+ line.strip!
402
+ if (not line.empty?) and (line !~ /^y(?:es)?$/i)
403
+ raise WorkflowError, 'Aborted'
404
+ end
405
+ download_digdag
406
+ end
407
+ end
408
+
409
+ def download_digdag
410
+ require 'net/http'
411
+ require 'openssl'
412
+
413
+ FileUtils.mkdir_p digdag_dir
414
+
415
+ if bundled_java?
416
+ download_java
417
+ end
418
+
419
+ Dir.mktmpdir do |download_dir|
420
+ indicator = Command::TimeBasedDownloadProgressIndicator.new(
421
+ 'Downloading workflow module...', Time.new.to_i, 2)
422
+ status = nil
423
+ download_path = File.join(download_dir, 'digdag')
424
+ File.open(download_path, 'wb') do |file|
425
+ status = Updater.stream_fetch(digdag_url, file) {
426
+ indicator.update
427
+ }
428
+ end
429
+ indicator.finish
430
+
431
+ $stdout.puts
432
+
433
+ unless status
434
+ raise WorkflowError, 'Failed to download workflow module.'
435
+ end
436
+
437
+ $stdout.print 'Installing workflow module... '
438
+ FileUtils.rm_rf(digdag_cli_tmp_path)
439
+ FileUtils.cp(download_path, digdag_cli_tmp_path)
440
+ FileUtils.chmod('a=xr', digdag_cli_tmp_path)
441
+ FileUtils.mv(digdag_cli_tmp_path, digdag_cli_path)
442
+ $stdout.puts 'done'
443
+ end
444
+ end
445
+ end
446
+ end
@@ -17,5 +17,19 @@ module TreasureData
17
17
  def on_mac?
18
18
  RUBY_PLATFORM =~ /-darwin\d/
19
19
  end
20
+
21
+ def on_64bit_os?
22
+ if on_windows?
23
+ if ENV.fetch('PROCESSOR_ARCHITECTURE', '').downcase.include? 'amd64'
24
+ return true
25
+ end
26
+ return ENV.has_key?('PROCESSOR_ARCHITEW6432')
27
+ else
28
+ require 'open3'
29
+ out, status = Open3.capture2('uname', '-m')
30
+ raise 'Failed to detect OS bitness' unless status.success?
31
+ return out.downcase.include? 'x86_64'
32
+ end
33
+ end
20
34
  end
21
35
  end
@@ -258,6 +258,7 @@ module ModuleDefinition
258
258
 
259
259
  def stream_fetch(url, binfile, &progress)
260
260
  require 'net/http'
261
+ require 'openssl'
261
262
 
262
263
  uri = URI(url)
263
264
  http_class = Command.get_http_class
@@ -268,7 +269,7 @@ module ModuleDefinition
268
269
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE
269
270
  end
270
271
 
271
- http.request_get(uri.path) {|response|
272
+ http.request_get(uri.path + (uri.query ? '?' + uri.query : '')) {|response|
272
273
  if response.class == Net::HTTPOK
273
274
  # $stdout.print a . every tick_period seconds
274
275
  response.read_body do |chunk|
@@ -1,3 +1,3 @@
1
1
  module TreasureData
2
- TOOLBELT_VERSION = '0.14.1'
2
+ TOOLBELT_VERSION = '0.15.0'
3
3
  end
@@ -353,5 +353,127 @@ module TreasureData::Command
353
353
  expect(stdout_io.string).to include table
354
354
  end
355
355
  end
356
+
357
+ describe '#connector_update' do
358
+ let(:name) { 'daily_mysql_import' }
359
+ let(:name2) { 'daily_mysql_import2' }
360
+ let(:cron) { '10 0 * * *' }
361
+ let(:cron2) { '20 0 * * *' }
362
+ let(:database) { 'td_sample_db' }
363
+ let(:table) { 'td_sample_table' }
364
+ let(:config_file) {
365
+ Tempfile.new('config.yml').tap {|tf|
366
+ tf.puts({"foo" => "bar"}.to_yaml)
367
+ tf.close
368
+ }
369
+ }
370
+ let(:config) {
371
+ h = YAML.load_file(config_file.path)
372
+ TreasureData::ConnectorConfigNormalizer.new(h).normalized_config
373
+ }
374
+ let(:config_diff_file) {
375
+ Tempfile.new('config_diff.yml').tap {|tf|
376
+ tf.puts({"hoge" => "fuga"}.to_yaml)
377
+ tf.close
378
+ }
379
+ }
380
+ let(:config_diff) {
381
+ h = YAML.load_file(config_diff_file.path)
382
+ TreasureData::ConnectorConfigNormalizer.new(h).normalized_config
383
+ }
384
+ let(:option) {
385
+ List::CommandParser.new("connector:update", %w(name), %w(config_file), nil, argv, true)
386
+ }
387
+ let(:response) {
388
+ {'name' => name, 'cron' => cron, 'timezone' => 'UTC', 'delay' => 0, 'database' => database, 'table' => table,
389
+ 'config' => config, 'config_diff' => config_diff}
390
+ }
391
+ let(:client) { double(:client) }
392
+
393
+ before do
394
+ allow(command).to receive(:get_client).and_return(client)
395
+ allow(client).to receive(:bulk_load_update) do |name, settings|
396
+ r = response.merge('name' => name)
397
+ settings.each do |key, value|
398
+ value = nil if key == 'cron' && value.empty?
399
+ r[key.to_s] = value
400
+ end
401
+ r
402
+ end
403
+ end
404
+
405
+ context 'with new_name' do
406
+ let (:argv){ ['--newname', name2, name] }
407
+ it 'show update result' do
408
+ expect{command.connector_update(option)}.not_to raise_error(SystemExit)
409
+ expect(stdout_io.string).to include name2
410
+ expect(stdout_io.string).to include cron
411
+ expect(stdout_io.string).to include database
412
+ expect(stdout_io.string).to include table
413
+ expect(YAML.load(stdout_io.string[/^Config\n---\n(.*?\n)\n/m, 1])).to eq(config)
414
+ expect(YAML.load(stdout_io.string[/^Config Diff\n---\n(.*?\n)\Z/m, 1])).to eq(config_diff)
415
+ end
416
+ end
417
+
418
+ context 'with config' do
419
+ let (:argv){ [name, config_file.path] }
420
+ it 'show update result' do
421
+ expect{command.connector_update(option)}.not_to raise_error(SystemExit)
422
+ expect(stdout_io.string).to include name
423
+ expect(stdout_io.string).to include cron
424
+ expect(stdout_io.string).to include database
425
+ expect(stdout_io.string).to include table
426
+ expect(YAML.load(stdout_io.string[/^Config\n---\n(.*?\n)\n/m, 1])).to eq(config)
427
+ expect(YAML.load(stdout_io.string[/^Config Diff\n---\n(.*?\n)\Z/m, 1])).to eq(config_diff)
428
+ end
429
+ end
430
+
431
+ context 'with config_diff' do
432
+ let(:argv){ ['--config-diff', config_diff_file.path, name] }
433
+ it 'show update result' do
434
+ expect{command.connector_update(option)}.not_to raise_error(SystemExit)
435
+ expect(stdout_io.string).to include name
436
+ expect(stdout_io.string).to include cron
437
+ expect(stdout_io.string).to include database
438
+ expect(stdout_io.string).to include table
439
+ expect(YAML.load(stdout_io.string[/^Config\n---\n(.*?\n)\n/m, 1])).to eq(config)
440
+ expect(YAML.load(stdout_io.string[/^Config Diff\n---\n(.*?\n)\Z/m, 1])).to eq(config_diff)
441
+ end
442
+ end
443
+
444
+ context 'with schedule' do
445
+ let(:argv) { ['--schedule', cron2, name] }
446
+ it 'can update cron' do
447
+ expect{command.connector_update(option)}.not_to raise_error(SystemExit)
448
+ expect(stdout_io.string).to include name
449
+ expect(stdout_io.string).to include cron2
450
+ expect(stdout_io.string).to include database
451
+ expect(stdout_io.string).to include table
452
+ expect(YAML.load(stdout_io.string[/^Config\n---\n(.*?\n)\n/m, 1])).to eq(config)
453
+ expect(YAML.load(stdout_io.string[/^Config Diff\n---\n(.*?\n)\Z/m, 1])).to eq(config_diff)
454
+ end
455
+ end
456
+
457
+ context 'with empty schedule' do
458
+ let(:argv) { [name, '--schedule'] }
459
+ it 'can update cron' do
460
+ expect{command.connector_update(option)}.not_to raise_error(SystemExit)
461
+ expect(stdout_io.string).to include name
462
+ expect(stdout_io.string).to include "Cron : \n"
463
+ expect(stdout_io.string).to include database
464
+ expect(stdout_io.string).to include table
465
+ expect(YAML.load(stdout_io.string[/^Config\n---\n(.*?\n)\n/m, 1])).to eq(config)
466
+ expect(YAML.load(stdout_io.string[/^Config Diff\n---\n(.*?\n)\Z/m, 1])).to eq(config_diff)
467
+ end
468
+ end
469
+
470
+ context 'nothing to update' do
471
+ let (:argv) { [name] }
472
+ it 'show update result' do
473
+ expect{command.connector_update(option)}.to raise_error(SystemExit)
474
+ expect(stdout_io.string).to include 'Error: nothing to update'
475
+ end
476
+ end
477
+ end
356
478
  end
357
479
  end
@@ -0,0 +1,314 @@
1
+ require 'spec_helper'
2
+ require 'td/command/common'
3
+ require 'td/command/list'
4
+ require 'td/command/workflow'
5
+
6
+ def java_available?
7
+ begin
8
+ output, status = Open3.capture2e('java', '-version')
9
+ rescue
10
+ return false
11
+ end
12
+ if not status.success?
13
+ return false
14
+ end
15
+ if output !~ /(openjdk|java) version "1/
16
+ return false
17
+ end
18
+ return true
19
+ end
20
+
21
+ STDERR.puts
22
+ STDERR.puts("RUBY_PLATFORM: #{RUBY_PLATFORM}")
23
+ STDERR.puts("on_64bit_os?: #{TreasureData::Helpers.on_64bit_os?}")
24
+ STDERR.puts("java_available?: #{java_available?}")
25
+ STDERR.puts
26
+
27
+ module TreasureData::Command
28
+
29
+ describe 'workflow command' do
30
+
31
+ let(:command) {
32
+ Class.new { include TreasureData::Command }.new
33
+ }
34
+ let(:stdout_io) { StringIO.new }
35
+ let(:stderr_io) { StringIO.new }
36
+ let(:home_env) { TreasureData::Helpers.on_windows? ? 'USERPROFILE' : 'HOME' }
37
+ let(:java_exe) { TreasureData::Helpers.on_windows? ? 'java.exe' : 'java' }
38
+
39
+ around do |example|
40
+
41
+ stdout = $stdout.dup
42
+ stderr = $stderr.dup
43
+
44
+ begin
45
+ $stdout = stdout_io
46
+ $stderr = stderr_io
47
+
48
+ Dir.mktmpdir { |home|
49
+ with_env(home_env, home) {
50
+ example.run
51
+ }
52
+ }
53
+ ensure
54
+ $stdout = stdout
55
+ $stderr = stderr
56
+ end
57
+ end
58
+
59
+ def with_env(name, var)
60
+ backup, ENV[name] = ENV[name], var
61
+ begin
62
+ yield
63
+ ensure
64
+ ENV[name] = backup
65
+ end
66
+ end
67
+
68
+ let(:tmpdir) {
69
+ Dir.mktmpdir
70
+ }
71
+
72
+ let(:project_name) {
73
+ 'foobar'
74
+ }
75
+
76
+ let(:project_dir) {
77
+ File.join(tmpdir, project_name)
78
+ }
79
+
80
+ let(:workflow_name) {
81
+ project_name
82
+ }
83
+
84
+ let(:workflow_file) {
85
+ File.join(project_dir, workflow_name + '.dig')
86
+ }
87
+
88
+ let(:td_conf) {
89
+ File.join(tmpdir, 'td.conf')
90
+ }
91
+
92
+ after(:each) {
93
+ FileUtils.rm_rf tmpdir
94
+ }
95
+
96
+ describe '#workflow' do
97
+ let(:empty_option) {
98
+ List::CommandParser.new("workflow", [], [], nil, [], true)
99
+ }
100
+
101
+ let(:version_option) {
102
+ List::CommandParser.new("workflow", [], [], nil, ['version'], true)
103
+ }
104
+
105
+ let(:init_option) {
106
+ List::CommandParser.new("workflow", [], [], nil, ['init', project_dir], true)
107
+ }
108
+
109
+ let(:run_option) {
110
+ List::CommandParser.new("workflow", [], [], nil, ['run', workflow_name], true)
111
+ }
112
+
113
+ let(:reset_option) {
114
+ List::CommandParser.new("workflow:reset", [], [], nil, [], true)
115
+ }
116
+
117
+ let(:version_option) {
118
+ List::CommandParser.new("workflow:version", [], [], nil, [], true)
119
+ }
120
+
121
+ let (:apikey) {
122
+ '4711/badf00d'
123
+ }
124
+
125
+ before(:each) {
126
+ allow(TreasureData::Config).to receive(:apikey) { apikey }
127
+ allow(TreasureData::Config).to receive(:path) { td_conf }
128
+ File.write(td_conf, [
129
+ '[account]',
130
+ ' user = test@example.com',
131
+ " apikey = #{apikey}",
132
+ ].join($/) + $/)
133
+ }
134
+
135
+ it 'complains about 32 bit platform if no usable java on path' do
136
+ allow(TreasureData::Helpers).to receive(:on_64bit_os?) { false }
137
+ with_env('PATH', '') do
138
+ expect { command.workflow(empty_option, capture_output=true) }.to raise_error(WorkflowError) { |error|
139
+ expect(error.message).to include(<<EOF
140
+ A suitable installed version of Java could not be found and and Java cannot be
141
+ automatically installed for this OS.
142
+
143
+ Please install at least Java 8u71.
144
+ EOF
145
+ )
146
+ }
147
+ end
148
+ end
149
+
150
+ it 'uses system java by default on 32 bit platforms' do
151
+ allow(TreasureData::Helpers).to receive(:on_64bit_os?) { false }
152
+ expect(java_available?).to be(true)
153
+
154
+ allow(TreasureData::Updater).to receive(:stream_fetch).and_call_original
155
+ allow($stdin).to receive(:gets) { 'Y' }
156
+ status = command.workflow(empty_option, capture_output=true)
157
+ expect(status).to be 0
158
+ expect(stdout_io.string).to_not include 'Downloading Java'
159
+ expect(stdout_io.string).to include 'Downloading workflow module'
160
+ expect(File).to exist(File.join(ENV[home_env], '.td', 'digdag', 'digdag'))
161
+ expect(TreasureData::Updater).to_not have_received(:stream_fetch).with(
162
+ %r{/java/}, instance_of(File))
163
+ expect(TreasureData::Updater).to have_received(:stream_fetch).with(
164
+ 'http://toolbelt.treasure-data.com/digdag?user=test%40example.com', instance_of(File))
165
+ end
166
+
167
+ it 'installs java + digdag and can run a workflow' do
168
+ skip 'Requires 64 bit OS or java' unless (TreasureData::Helpers::on_64bit_os? or java_available?)
169
+
170
+ allow(TreasureData::Updater).to receive(:stream_fetch).and_call_original
171
+ allow($stdin).to receive(:gets) { 'Y' }
172
+ status = command.workflow(empty_option, capture_output=true)
173
+ expect(status).to be 0
174
+ if TreasureData::Helpers::on_64bit_os?
175
+ expect(stdout_io.string).to include 'Downloading Java'
176
+ expect(File).to exist(File.join(ENV[home_env], '.td', 'digdag', 'jre', 'bin', java_exe))
177
+ expect(TreasureData::Updater).to have_received(:stream_fetch).with(
178
+ %r{/java/}, instance_of(File))
179
+ end
180
+ expect(stdout_io.string).to include 'Downloading workflow module'
181
+ expect(File).to exist(File.join(ENV[home_env], '.td', 'digdag', 'digdag'))
182
+ expect(TreasureData::Updater).to have_received(:stream_fetch).with(
183
+ 'http://toolbelt.treasure-data.com/digdag?user=test%40example.com', instance_of(File))
184
+
185
+ # Check that java and digdag is not re-installed
186
+ stdout_io.truncate(0)
187
+ stderr_io.truncate(0)
188
+ status = command.workflow(empty_option, capture_output=true)
189
+ expect(status).to be 0
190
+ expect(stdout_io.string).to_not include 'Downloading Java'
191
+ expect(stdout_io.string).to_not include 'Downloading workflow module'
192
+
193
+ # Generate a new project
194
+ expect(Dir.exist? project_dir).to be(false)
195
+ stdout_io.truncate(0)
196
+ stderr_io.truncate(0)
197
+ status = command.workflow(init_option, capture_output=true)
198
+ expect(status).to be 0
199
+ expect(stdout_io.string).to include('Creating')
200
+ expect(Dir.exist? project_dir).to be(true)
201
+ expect(File.exist? workflow_file).to be(true)
202
+
203
+ # Run a workflow
204
+ File.write(workflow_file, <<EOF
205
+ +main:
206
+ echo>: hello world
207
+ EOF
208
+ )
209
+ Dir.chdir(project_dir) {
210
+ stdout_io.truncate(0)
211
+ stderr_io.truncate(0)
212
+ status = command.workflow(run_option, capture_output=true)
213
+ expect(status).to be 0
214
+ expect(stderr_io.string).to include('Success')
215
+ expect(stdout_io.string).to include('hello world')
216
+ }
217
+ end
218
+
219
+ it 'uses specified java and installs digdag' do
220
+ with_env('TD_WF_JAVA', 'java') {
221
+ allow(TreasureData::Updater).to receive(:stream_fetch).and_call_original
222
+ allow($stdin).to receive(:gets) { 'Y' }
223
+ status = command.workflow(empty_option, capture_output=true)
224
+ expect(status).to be 0
225
+ expect(stdout_io.string).to_not include 'Downloading Java'
226
+ expect(stdout_io.string).to include 'Downloading workflow module'
227
+ expect(File).to exist(File.join(ENV[home_env], '.td', 'digdag', 'digdag'))
228
+ expect(TreasureData::Updater).to_not have_received(:stream_fetch).with(
229
+ %r{/java/}, instance_of(File))
230
+ expect(TreasureData::Updater).to have_received(:stream_fetch).with(
231
+ 'http://toolbelt.treasure-data.com/digdag?user=test%40example.com', instance_of(File))
232
+
233
+ # Check that digdag is not re-installed
234
+ stdout_io.truncate(0)
235
+ stderr_io.truncate(0)
236
+ status = command.workflow(empty_option, capture_output=true)
237
+ expect(status).to be 0
238
+ expect(stdout_io.string).to_not include 'Downloading Java'
239
+ expect(stdout_io.string).to_not include 'Downloading workflow module'
240
+ }
241
+ end
242
+
243
+ it 'reinstalls cleanly after reset' do
244
+ skip 'Requires 64 bit OS or system java' unless (TreasureData::Helpers::on_64bit_os? or java_available?)
245
+
246
+ # First install
247
+ allow($stdin).to receive(:gets) { 'Y' }
248
+ status = command.workflow(empty_option, capture_output=true)
249
+ expect(status).to be 0
250
+ expect(stderr_io.string).to include 'Digdag v'
251
+ expect(File).to exist(File.join(ENV[home_env], '.td', 'digdag'))
252
+
253
+ # Reset
254
+ stdout_io.truncate(0)
255
+ stderr_io.truncate(0)
256
+ status = command.workflow_reset(reset_option)
257
+ expect(status).to be 0
258
+ expect(File).to_not exist(File.join(ENV[home_env], '.td', 'digdag'))
259
+ expect(File).to exist(File.join(ENV[home_env], '.td'))
260
+ expect(stdout_io.string).to include 'Removing workflow module...'
261
+ expect(stdout_io.string).to include 'Done'
262
+
263
+ # Reinstall
264
+ allow($stdin).to receive(:gets) { 'Y' }
265
+ stdout_io.truncate(0)
266
+ stderr_io.truncate(0)
267
+ status = command.workflow(empty_option, capture_output=true)
268
+ expect(status).to be 0
269
+ expect(stderr_io.string).to include 'Digdag v'
270
+ expect(File).to exist(File.join(ENV[home_env], '.td', 'digdag'))
271
+ end
272
+
273
+ it 'uses -k apikey' do
274
+ with_env('TD_WF_JAVA', 'echo') {
275
+ allow(TreasureData::Config).to receive(:cl_apikey) { true }
276
+ stdout_io.truncate(0)
277
+ stderr_io.truncate(0)
278
+ status = command.workflow(init_option, capture_output=true, check_prereqs=false)
279
+ expect(status).to be 0
280
+ expect(stdout_io.string).to include('--config')
281
+ expect(stdout_io.string).to_not include('io.digdag.standards.td.client-configurator.enabled=true')
282
+ }
283
+ end
284
+
285
+ it 'complains if there is no apikey' do
286
+ allow(TreasureData::Config).to receive(:apikey) { nil}
287
+ expect{command.workflow(version_option)}.to raise_error(TreasureData::ConfigError)
288
+ end
289
+
290
+ it 'prints the java and digdag versions' do
291
+ skip 'Requires 64 bit OS' unless TreasureData::Helpers::on_64bit_os?
292
+
293
+ # Not yet installed
294
+ status = command.workflow_version(version_option)
295
+ expect(status).to be 1
296
+ expect(stderr_io.string).to include 'Workflow module not yet installed.'
297
+
298
+ # Install
299
+ allow($stdin).to receive(:gets) { 'Y' }
300
+ status = command.workflow(empty_option, capture_output=true)
301
+ expect(status).to be 0
302
+
303
+ # Check that version is shown
304
+ stdout_io.truncate(0)
305
+ stderr_io.truncate(0)
306
+ status = command.workflow_version(version_option)
307
+ expect(status).to be 0
308
+ expect(stdout_io.string).to include 'Bundled Java: true'
309
+ expect(stdout_io.string).to include 'openjdk version'
310
+ expect(stdout_io.string).to include 'Digdag version:'
311
+ end
312
+ end
313
+ end
314
+ end
@@ -1,7 +1,9 @@
1
1
  require 'spec_helper'
2
2
  require 'td/helpers'
3
+ require 'open3'
3
4
 
4
5
  module TreasureData
6
+
5
7
  describe 'format_with_delimiter' do
6
8
  it "delimits the number with ',' by default" do
7
9
  expect(Helpers.format_with_delimiter(0)).to eq("0")
@@ -13,4 +15,57 @@ module TreasureData
13
15
  expect(Helpers.format_with_delimiter(1000000)).to eq("1,000,000")
14
16
  end
15
17
  end
18
+
19
+ describe 'on_64bit_os?' do
20
+
21
+ def with_env(name, var)
22
+ backup, ENV[name] = ENV[name], var
23
+ begin
24
+ yield
25
+ ensure
26
+ ENV[name] = backup
27
+ end
28
+ end
29
+
30
+ it 'returns true for windows when PROCESSOR_ARCHITECTURE=amd64' do
31
+ allow(Helpers).to receive(:on_windows?) {true}
32
+ with_env('PROCESSOR_ARCHITECTURE', 'amd64') {
33
+ expect(Helpers.on_64bit_os?).to be(true)
34
+ }
35
+ end
36
+
37
+ it 'returns true for windows when PROCESSOR_ARCHITECTURE=x86 and PROCESSOR_ARCHITEW6432 is set' do
38
+ allow(Helpers).to receive(:on_windows?) {true}
39
+ with_env('PROCESSOR_ARCHITECTURE', 'x86') {
40
+ with_env('PROCESSOR_ARCHITEW6432', '') {
41
+ expect(Helpers.on_64bit_os?).to be(true)
42
+ }
43
+ }
44
+ end
45
+
46
+ it 'returns false for windows when PROCESSOR_ARCHITECTURE=x86 and PROCESSOR_ARCHITEW6432 is not set' do
47
+ allow(Helpers).to receive(:on_windows?) {true}
48
+ with_env('PROCESSOR_ARCHITECTURE', 'x86') {
49
+ with_env('PROCESSOR_ARCHITEW6432', nil) {
50
+ expect(Helpers.on_64bit_os?).to be(false)
51
+ }
52
+ }
53
+ end
54
+
55
+ it 'returns true for non-windows when uname -m prints x86_64' do
56
+ allow(Helpers).to receive(:on_windows?) {false}
57
+ allow(Open3).to receive(:capture2).with('uname', '-m') {['x86_64', double(:success? => true)]}
58
+ expect(Helpers.on_64bit_os?).to be(true)
59
+ expect(Open3).to have_received(:capture2).with('uname', '-m')
60
+ end
61
+
62
+ it 'returns false for non-windows when uname -m prints i686' do
63
+ allow(Helpers).to receive(:on_windows?) {false}
64
+ allow(Open3).to receive(:capture2).with('uname', '-m') {['i686', double(:success? => true)]}
65
+ expect(Helpers.on_64bit_os?).to be(false)
66
+ expect(Open3).to have_received(:capture2).with('uname', '-m')
67
+ end
68
+
69
+ end
70
+
16
71
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: td
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Treasure Data, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-11 00:00:00.000000000 Z
11
+ date: 2016-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -269,6 +269,7 @@ files:
269
269
  - lib/td/command/table.rb
270
270
  - lib/td/command/update.rb
271
271
  - lib/td/command/user.rb
272
+ - lib/td/command/workflow.rb
272
273
  - lib/td/compact_format_yamler.rb
273
274
  - lib/td/compat_core.rb
274
275
  - lib/td/compat_gzip_reader.rb
@@ -293,6 +294,7 @@ files:
293
294
  - spec/td/command/query_spec.rb
294
295
  - spec/td/command/sched_spec.rb
295
296
  - spec/td/command/table_spec.rb
297
+ - spec/td/command/workflow_spec.rb
296
298
  - spec/td/common_spec.rb
297
299
  - spec/td/compact_format_yamler_spec.rb
298
300
  - spec/td/connector_config_normalizer_spec.rb
@@ -343,6 +345,7 @@ test_files:
343
345
  - spec/td/command/query_spec.rb
344
346
  - spec/td/command/sched_spec.rb
345
347
  - spec/td/command/table_spec.rb
348
+ - spec/td/command/workflow_spec.rb
346
349
  - spec/td/common_spec.rb
347
350
  - spec/td/compact_format_yamler_spec.rb
348
351
  - spec/td/connector_config_normalizer_spec.rb