gooddata 0.6.50 → 0.6.51

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.editorconfig +12 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +3 -0
  5. data/gooddata.gemspec +2 -1
  6. data/lib/gooddata/bricks/middleware/aws_middleware.rb +4 -0
  7. data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +1 -1
  8. data/lib/gooddata/bricks/middleware/dwh_middleware.rb +1 -0
  9. data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +1 -0
  10. data/lib/gooddata/bricks/middleware/logger_middleware.rb +2 -1
  11. data/lib/gooddata/core/nil_logger.rb +9 -0
  12. data/lib/gooddata/goodzilla/goodzilla.rb +1 -1
  13. data/lib/gooddata/helpers/data_helper.rb +1 -0
  14. data/lib/gooddata/helpers/global_helpers_params.rb +54 -27
  15. data/lib/gooddata/lcm/actions/apply_custom_maql.rb +70 -0
  16. data/lib/gooddata/lcm/actions/associate_clients.rb +17 -4
  17. data/lib/gooddata/lcm/actions/collect_clients.rb +4 -1
  18. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +1 -0
  19. data/lib/gooddata/lcm/actions/collect_segments.rb +15 -2
  20. data/lib/gooddata/lcm/actions/create_segment_masters.rb +2 -2
  21. data/lib/gooddata/lcm/actions/provision_clients.rb +2 -4
  22. data/lib/gooddata/lcm/actions/purge_clients.rb +2 -2
  23. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +81 -0
  24. data/lib/gooddata/lcm/actions/synchronize_label_types.rb +2 -2
  25. data/lib/gooddata/lcm/actions/synchronize_processes.rb +3 -1
  26. data/lib/gooddata/lcm/data/create_lcm_release.sql.erb +2 -1
  27. data/lib/gooddata/lcm/helpers/check_helper.rb +1 -1
  28. data/lib/gooddata/lcm/lcm.rb +29 -11
  29. data/lib/gooddata/lcm/lcm2.rb +82 -20
  30. data/lib/gooddata/models/domain.rb +22 -1
  31. data/lib/gooddata/models/metadata.rb +13 -8
  32. data/lib/gooddata/models/metadata/attribute.rb +1 -1
  33. data/lib/gooddata/models/metadata/report_definition.rb +1 -0
  34. data/lib/gooddata/models/profile.rb +1 -1
  35. data/lib/gooddata/models/project.rb +162 -38
  36. data/lib/gooddata/models/project_creator.rb +26 -6
  37. data/lib/gooddata/models/project_log_formatter.rb +204 -0
  38. data/lib/gooddata/models/schedule.rb +2 -21
  39. data/lib/gooddata/models/segment.rb +26 -0
  40. data/lib/gooddata/models/style_setting.rb +5 -1
  41. data/lib/gooddata/models/user_filters/user_filter_builder.rb +9 -0
  42. data/lib/gooddata/rest/connection.rb +4 -1
  43. data/lib/gooddata/version.rb +1 -1
  44. data/spec/environment/development.rb +29 -0
  45. data/spec/environment/environment.rb +14 -2
  46. data/spec/environment/{hotfix.rb → testing.rb} +0 -0
  47. data/spec/integration/date_dim_switch_spec.rb +3 -5
  48. data/spec/integration/lcm_spec.rb +24 -21
  49. data/spec/integration/project_spec.rb +16 -0
  50. data/spec/integration/segments_spec.rb +1 -1
  51. data/spec/unit/helpers/global_helpers_spec.rb +26 -2
  52. data/spec/unit/helpers_spec.rb +20 -0
  53. data/spec/unit/models/project_creator_spec.rb +3 -2
  54. metadata +29 -12
  55. data/lib/gooddata/lcm/actions/ensure_titles.rb +0 -54
  56. data/spec/environment/develop.rb +0 -46
@@ -55,10 +55,10 @@ module GoodData
55
55
  project = segment_client.project
56
56
  res = {
57
57
  client_id: segment_client.client_id,
58
- project: project.pid
58
+ project: project && project.pid
59
59
  }
60
60
 
61
- if project.deleted?
61
+ if project.nil? || project.deleted?
62
62
  segment_client.delete
63
63
  res[:status] = 'purged'
64
64
  else
@@ -0,0 +1,81 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ require_relative 'base_action'
8
+
9
+ module GoodData
10
+ module LCM2
11
+ class SynchronizeETLsInSegment < BaseAction
12
+ DESCRIPTION = 'Synchronize ETLs (CC/Ruby) In Segment'
13
+
14
+ PARAMS = define_params(self) do
15
+ description 'Client Used for Connecting to GD'
16
+ param :gdc_gd_client, instance_of(Type::GdClientType), required: true
17
+ end
18
+
19
+ # will be updated later based on the way etl synchronization
20
+ RESULT_HEADER = [
21
+ :segment,
22
+ :master_project,
23
+ :client_id,
24
+ :client_project,
25
+ :status
26
+ ]
27
+
28
+ class << self
29
+ def call(params)
30
+ # Check if all required parameters were passed
31
+ BaseAction.check_params(PARAMS, params)
32
+
33
+ client = params.gdc_gd_client
34
+ domain_name = params.organization || params.domain
35
+ domain = client.domain(domain_name) || fail("Invalid domain name specified - #{domain_name}")
36
+ synchronize_segments = params.synchronize.group_by do |info|
37
+ info[:segment_id]
38
+ end
39
+
40
+ results = synchronize_segments.flat_map do |segment_id, synchronize|
41
+ segment = domain.segments(segment_id)
42
+ res = segment.synchronize_processes(
43
+ synchronize.flat_map do |info|
44
+ info[:to].flat_map do |to|
45
+ to[:pid]
46
+ end
47
+ end
48
+ )
49
+
50
+ res = GoodData::Helpers.symbolize_keys(res)
51
+ res[:syncedResult][:clients].flat_map do |item|
52
+ item = item[:client]
53
+ {
54
+ segment: segment_id,
55
+ master_project: segment.master_project_id,
56
+ client_id: item[:id],
57
+ client_project: item[:project].split('/').last,
58
+ status: 'ok'
59
+ }
60
+ end
61
+ end
62
+
63
+ params.synchronize.each do |info|
64
+ to_projects = info.to
65
+ to_projects.each do |entry|
66
+ pid = entry[:pid]
67
+ to_project = client.projects(pid) || fail("Invalid 'to' project specified - '#{pid}'")
68
+ to_project.schedules.each do |schedule|
69
+ schedule.update_params(params.additional_params || {})
70
+ schedule.update_hidden_params(params.additional_hidden_params || {})
71
+ schedule.save
72
+ end
73
+ end
74
+ end
75
+
76
+ results
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -37,8 +37,8 @@ module GoodData
37
37
  to = info.to
38
38
 
39
39
  from_project = development_client.projects(from) || fail("Invalid 'from' project specified - '#{from}'")
40
- to_projects = to.map do |pid|
41
- client.projects(pid)
40
+ to_projects = to.map do |project|
41
+ client.projects(project.pid)
42
42
  end
43
43
 
44
44
  GoodData::LCM.transfer_label_types(from_project, to_projects)
@@ -14,6 +14,8 @@ module GoodData
14
14
  PARAMS = define_params(self) do
15
15
  description 'Client Used for Connecting to GD'
16
16
  param :gdc_gd_client, instance_of(Type::GdClientType), required: true
17
+ description 'Uri of the source output stage. It must be in the same domain as the target project.'
18
+ param :ads_output_stage_uri, instance_of(Type::StringType), required: false
17
19
  end
18
20
 
19
21
  RESULT_HEADER = [
@@ -45,7 +47,7 @@ module GoodData
45
47
  to_project = client.projects(pid) || fail("Invalid 'to' project specified - '#{pid}'")
46
48
 
47
49
  params.gdc_logger.info "Transferring processes, from project: '#{from.title}', PID: '#{from.pid}', to project: '#{to_project.title}', PID: '#{to_project.pid}'"
48
- GoodData::Project.transfer_processes(from, to_project)
50
+ GoodData::Project.transfer_processes(from, to_project, ads_output_stage_uri: params.ads_output_stage_uri)
49
51
 
50
52
  to_project.add.output_stage.client_id = client_id if client_id && to_project.add.output_stage
51
53
 
@@ -1,5 +1,6 @@
1
1
  CREATE TABLE IF NOT EXISTS "<%= table_name || 'LCM_RELEASE' %>" (
2
2
  segment_id VARCHAR(255) NOT NULL,
3
3
  master_project_id VARCHAR(255) NOT NULL,
4
- version INTEGER NOT NULL
4
+ version INTEGER NOT NULL,
5
+ maql_ddl VARCHAR(65000)
5
6
  );
@@ -12,7 +12,7 @@ module GoodData
12
12
  class << self
13
13
  def check_params(specification, params)
14
14
  specification.keys.each do |param_name|
15
- value = params[param_name]
15
+ value = params.send(param_name)
16
16
  type = specification[param_name][:type]
17
17
 
18
18
  if value.nil?
@@ -146,6 +146,15 @@ module GoodData
146
146
  end
147
147
  end
148
148
  end
149
+
150
+ # User groups must be migrated after dashboards
151
+ puts 'Migrating User Groups'
152
+ domain.clients.peach do |c|
153
+ segment = c.segment
154
+ segment_master = segment.master_project
155
+ project = c.project
156
+ GoodData::Project.transfer_user_groups(segment_master, project)
157
+ end
149
158
  end
150
159
 
151
160
  def transfer_label_types(source_project, targets)
@@ -251,30 +260,39 @@ module GoodData
251
260
 
252
261
  # Generate transfer table
253
262
  drill_paths = attributes.map do |attribute|
254
- [attribute.meta['identifier'], attribute.content['drillDownStepAttributeDF']]
263
+ drill_label_uri = attribute.content['drillDownStepAttributeDF']
264
+ if drill_label_uri
265
+ drill_label = source_project.labels(drill_label_uri)
266
+ [attribute.meta['identifier'], drill_label.identifier]
267
+ else
268
+ []
269
+ end
255
270
  end
256
271
  transfer = Hash[*drill_paths.flatten].compact
257
272
 
258
273
  # Transfer to target projects
259
274
  targets.peach do |target|
260
- transfer.peach do |identifier, drill_path|
261
- uri = GoodData::MdObject.identifier_to_uri({ project: target, client: target.client }, identifier)
262
- next unless uri
275
+ transfer.peach do |attr_identifier, drill_path_identifier|
276
+ attr_uri = GoodData::MdObject.identifier_to_uri({ project: target, client: target.client }, attr_identifier)
277
+ next unless attr_uri
263
278
 
264
- obj = GoodData::MdObject[uri, { project: target, client: target.client }]
279
+ attribute = GoodData::MdObject[attr_uri, project: target, client: target.client]
265
280
 
266
- if obj
267
- if !obj.content['drillDownStepAttributeDF'] || obj.content['drillDownStepAttributeDF'] != drill_path
281
+ if attribute
282
+ drill_path = GoodData::MdObject.identifier_to_uri({ project: target, client: target.client }, drill_path_identifier)
283
+ next unless drill_path
284
+
285
+ if !attribute.content['drillDownStepAttributeDF'] || attribute.content['drillDownStepAttributeDF'] != drill_path
268
286
  semaphore.synchronize do
269
- GoodData.logger.info "Updating drill path of #{identifier} -> #{drill_path} in '#{target.title}'"
287
+ GoodData.logger.info "Updating drill path of #{attr_identifier} -> #{drill_path} in '#{target.title}'"
270
288
  end
271
289
 
272
- obj.content['drillDownStepAttributeDF'] = drill_path
273
- obj.save
290
+ attribute.content['drillDownStepAttributeDF'] = drill_path
291
+ attribute.save
274
292
  end
275
293
  else
276
294
  semaphore.synchronize do
277
- GoodData.logger.warn "Unable to find #{identifier} in '#{target.title}'"
295
+ GoodData.logger.warn "Unable to find #{attr_identifier} in '#{target.title}'"
278
296
  end
279
297
  end
280
298
 
@@ -16,8 +16,16 @@ module GoodData
16
16
  def method_missing(name, *_args)
17
17
  key = name.to_s.downcase.to_sym
18
18
 
19
- if key?(key)
20
- self[key]
19
+ value = nil
20
+ keys.each do |k|
21
+ if k.to_s.downcase.to_sym == key
22
+ value = self[k]
23
+ break
24
+ end
25
+ end
26
+
27
+ if value
28
+ value
21
29
  else
22
30
  begin
23
31
  super
@@ -27,6 +35,16 @@ module GoodData
27
35
  end
28
36
  end
29
37
 
38
+ def key?(key)
39
+ return true if super
40
+
41
+ keys.each do |k|
42
+ return true if k.to_s.downcase.to_sym == key.to_s.downcase.to_sym
43
+ end
44
+
45
+ false
46
+ end
47
+
30
48
  def respond_to_missing?(name, *_args)
31
49
  key = name.to_s.downcase.to_sym
32
50
  key?(key)
@@ -68,6 +86,7 @@ module GoodData
68
86
  EnsureTechnicalUsersProject,
69
87
  SynchronizeLdm,
70
88
  SynchronizeMeta,
89
+ SynchronizeLabelTypes,
71
90
  SynchronizeAttributeDrillpath,
72
91
  SynchronizeProcesses,
73
92
  SynchronizeSchedules,
@@ -86,10 +105,9 @@ module GoodData
86
105
  ProvisionClients,
87
106
  EnsureTechnicalUsersDomain,
88
107
  EnsureTechnicalUsersProject,
89
- EnsureTitles,
90
108
  SynchronizeAttributeDrillpath,
91
- SynchronizeProcesses,
92
- SynchronizeSchedules
109
+ SynchronizeETLsInSegment,
110
+ SynchronizeColorPalette
93
111
  ],
94
112
 
95
113
  rollout: [
@@ -102,9 +120,10 @@ module GoodData
102
120
  SynchronizeLdm,
103
121
  # SynchronizeLabelTypes,
104
122
  SynchronizeAttributeDrillpath,
105
- SynchronizeProcesses,
106
- SynchronizeSchedules,
107
- SynchronizeClients
123
+ ApplyCustomMaql,
124
+ SynchronizeColorPalette,
125
+ SynchronizeClients,
126
+ SynchronizeETLsInSegment
108
127
  ]
109
128
  }
110
129
 
@@ -122,9 +141,9 @@ module GoodData
122
141
  res = SmartHash.new
123
142
  params.each_pair do |k, v|
124
143
  if v.is_a?(Hash) || v.is_a?(Array)
125
- res[k.downcase] = convert_to_smart_hash(v)
144
+ res[k] = convert_to_smart_hash(v)
126
145
  else
127
- res[k.downcase] = v
146
+ res[k] = v
128
147
  end
129
148
  end
130
149
  res
@@ -138,7 +157,13 @@ module GoodData
138
157
  end
139
158
 
140
159
  def get_mode_actions(mode)
141
- MODES[mode.to_sym] || fail("Invalid mode specified '#{mode}', supported modes are: '#{MODE_NAMES.join(', ')}'")
160
+ mode = mode.to_sym
161
+ actions = MODES[mode]
162
+ if mode == :generic_lifecycle
163
+ []
164
+ else
165
+ actions || fail("Invalid mode specified '#{mode}', supported modes are: '#{MODE_NAMES.join(', ')}'")
166
+ end
142
167
  end
143
168
 
144
169
  def print_action_names(mode, actions)
@@ -166,12 +191,13 @@ module GoodData
166
191
  keys = if action.const_defined?('RESULT_HEADER')
167
192
  action.const_get('RESULT_HEADER')
168
193
  else
194
+ GoodData.logger.warn("Action #{action.name} does not have RESULT_HEADERS, inferring headers from results.")
169
195
  (messages.first && messages.first.keys) || []
170
196
  end
171
197
 
172
198
  headings = keys.map(&:upcase)
173
199
 
174
- rows = messages.map do |message|
200
+ rows = messages && messages.map do |message|
175
201
  row = []
176
202
  keys.each do |heading|
177
203
  row << message[heading]
@@ -179,6 +205,8 @@ module GoodData
179
205
  row
180
206
  end
181
207
 
208
+ rows ||= []
209
+
182
210
  table = Terminal::Table.new :title => title, :headings => headings do |t|
183
211
  rows.each_with_index do |row, index|
184
212
  t << row
@@ -202,6 +230,17 @@ module GoodData
202
230
 
203
231
  # Get actions for mode specified
204
232
  actions = get_mode_actions(mode)
233
+ if params.actions
234
+ actions = params.actions.map do |action|
235
+ "GoodData::LCM2::#{action}".split('::').inject(Object) do |o, c|
236
+ begin
237
+ o.const_get(c)
238
+ rescue NameError
239
+ fail NameError, "Cannot find action 'GoodData::LCM2::#{action}'"
240
+ end
241
+ end
242
+ end
243
+ end
205
244
 
206
245
  # Print name of actions to be performed for debug purposes
207
246
  print_action_names(mode, actions)
@@ -210,12 +249,34 @@ module GoodData
210
249
 
211
250
  new_params = params
212
251
 
252
+ fail_early = if params.key?(:fail_early)
253
+ params.fail_early.to_b
254
+ else
255
+ true
256
+ end
257
+
258
+ strict_mode = if params.key?(:strict)
259
+ params.strict.to_b
260
+ else
261
+ true
262
+ end
263
+
213
264
  # Run actions
214
- results = actions.map do |action|
265
+ errors = []
266
+ results = []
267
+ actions.each do |action|
215
268
  puts
216
269
 
217
270
  # Invoke action
218
- out = action.send(:call, params)
271
+ begin
272
+ out = action.send(:call, params)
273
+ rescue => e
274
+ errors << {
275
+ action: action,
276
+ err: e
277
+ }
278
+ break if fail_early
279
+ end
219
280
 
220
281
  # Handle output results and params
221
282
  res = out.is_a?(Array) ? out : out[:results]
@@ -229,10 +290,13 @@ module GoodData
229
290
  puts
230
291
  print_action_result(action, res)
231
292
 
232
- # Return result for final summary
233
- res
293
+ # Store result for final summary
294
+ results << res
234
295
  end
235
296
 
297
+ # Fail whole execution if there is any failed action
298
+ fail(JSON.pretty_generate(errors)) if strict_mode && errors.any?
299
+
236
300
  if actions.length > 1
237
301
  puts
238
302
  puts 'SUMMARY'
@@ -244,13 +308,11 @@ module GoodData
244
308
 
245
309
  brick_results = {}
246
310
  actions.each_with_index do |action, index|
247
- brick_results[action.class.short_name] = results[index]
311
+ brick_results[action.short_name] = results[index]
248
312
  end
249
313
 
250
314
  {
251
- actions: actions.map do |action|
252
- action.class.short_name
253
- end,
315
+ actions: actions.map(&:short_name),
254
316
  results: brick_results,
255
317
  params: params
256
318
  }
@@ -241,19 +241,30 @@ module GoodData
241
241
 
242
242
  list.pmapcat do |user|
243
243
  begin
244
- user_data = user.to_hash.tap { |uh| uh[:login].downcase }
244
+ user_data = user.to_hash.tap { |uh| uh[:login].downcase! }
245
245
  domain_user = domain_users_cache[user_data[:login]]
246
+ user_login = user_data[:login]
246
247
  if !domain_user
247
248
  added_user = domain.add_user(user_data, opts)
249
+ GoodData.logger.info("Added new user=#{user_login} to domain=#{default_domain_name}.")
248
250
  [{ type: :successful, :action => :user_added_to_domain, user: added_user }]
249
251
  else
252
+ domain_user_data = domain_user.to_hash
250
253
  fields_to_check = opts[:fields_to_check] || user_data.keys
251
254
  diff = GoodData::Helpers.diff([domain_user.to_hash], [user_data], key: :login, fields: fields_to_check)
252
255
  next [] if diff[:changed].empty?
253
256
  updated_user = domain.update_user(domain_user.to_hash.merge(user_data.compact), opts)
257
+ GoodData.logger.debug "Updated user=#{user_login} from old properties \
258
+ (email=#{domain_user_data[:email]}, sso_provider=#{domain_user_data[:sso_provider]}) \
259
+ to new properties (email=#{user_data[:email]}, sso_provider=#{user_data[:sso_provider]}) in domain=#{default_domain_name}."
254
260
  [{ type: :successful, :action => :user_changed_in_domain, user: updated_user }]
255
261
  end
256
262
  rescue RuntimeError => e
263
+ if !domain_user
264
+ GoodData.logger.error("Failed to add user=#{user_login} to domain=#{default_domain_name}. Error: #{e.message}")
265
+ else
266
+ GoodData.logger.error("Failed to update user=#{user_login} in domain=#{default_domain_name}. Error: #{e.message}")
267
+ end
257
268
  [{ type: :failed, :user => user, message: e }]
258
269
  end
259
270
  end
@@ -431,6 +442,10 @@ module GoodData
431
442
  alias_method :add_clients_settings, :update_clients_settings
432
443
 
433
444
  def update_clients(data, options = {})
445
+ if options[:delete_extra] && options[:delete_extra_in_segments]
446
+ fail 'Options delete_extra and delete_extra_in_segments are mutually exclusive.'
447
+ end
448
+
434
449
  delete_projects = options[:delete_projects] == false ? false : true
435
450
  payload = data.map do |datum|
436
451
  {
@@ -444,6 +459,12 @@ module GoodData
444
459
  end
445
460
  if options[:delete_extra] == true
446
461
  res = client.post(segments_uri + '/updateClients?deleteExtra=true', updateClients: { items: payload })
462
+ elsif options[:delete_extra_in_segments]
463
+ segments_to_delete_in = options[:delete_extra_in_segments]
464
+ .map { |segment| CGI.escape(segment) }
465
+ .join(',')
466
+ uri = segments_uri + "/updateClients?deleteExtraInSegments=#{segments_to_delete_in}"
467
+ res = client.post(uri, updateClients: { items: payload })
447
468
  else
448
469
  res = client.post(segments_uri + '/updateClients', updateClients: { items: payload })
449
470
  end