td 0.14.1 → 0.15.0

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 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