gooddata 0.6.50 → 0.6.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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