gooddata 0.6.50 → 0.6.51
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +12 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +3 -0
- data/gooddata.gemspec +2 -1
- data/lib/gooddata/bricks/middleware/aws_middleware.rb +4 -0
- data/lib/gooddata/bricks/middleware/decode_params_middleware.rb +1 -1
- data/lib/gooddata/bricks/middleware/dwh_middleware.rb +1 -0
- data/lib/gooddata/bricks/middleware/gooddata_middleware.rb +1 -0
- data/lib/gooddata/bricks/middleware/logger_middleware.rb +2 -1
- data/lib/gooddata/core/nil_logger.rb +9 -0
- data/lib/gooddata/goodzilla/goodzilla.rb +1 -1
- data/lib/gooddata/helpers/data_helper.rb +1 -0
- data/lib/gooddata/helpers/global_helpers_params.rb +54 -27
- data/lib/gooddata/lcm/actions/apply_custom_maql.rb +70 -0
- data/lib/gooddata/lcm/actions/associate_clients.rb +17 -4
- data/lib/gooddata/lcm/actions/collect_clients.rb +4 -1
- data/lib/gooddata/lcm/actions/collect_segment_clients.rb +1 -0
- data/lib/gooddata/lcm/actions/collect_segments.rb +15 -2
- data/lib/gooddata/lcm/actions/create_segment_masters.rb +2 -2
- data/lib/gooddata/lcm/actions/provision_clients.rb +2 -4
- data/lib/gooddata/lcm/actions/purge_clients.rb +2 -2
- data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +81 -0
- data/lib/gooddata/lcm/actions/synchronize_label_types.rb +2 -2
- data/lib/gooddata/lcm/actions/synchronize_processes.rb +3 -1
- data/lib/gooddata/lcm/data/create_lcm_release.sql.erb +2 -1
- data/lib/gooddata/lcm/helpers/check_helper.rb +1 -1
- data/lib/gooddata/lcm/lcm.rb +29 -11
- data/lib/gooddata/lcm/lcm2.rb +82 -20
- data/lib/gooddata/models/domain.rb +22 -1
- data/lib/gooddata/models/metadata.rb +13 -8
- data/lib/gooddata/models/metadata/attribute.rb +1 -1
- data/lib/gooddata/models/metadata/report_definition.rb +1 -0
- data/lib/gooddata/models/profile.rb +1 -1
- data/lib/gooddata/models/project.rb +162 -38
- data/lib/gooddata/models/project_creator.rb +26 -6
- data/lib/gooddata/models/project_log_formatter.rb +204 -0
- data/lib/gooddata/models/schedule.rb +2 -21
- data/lib/gooddata/models/segment.rb +26 -0
- data/lib/gooddata/models/style_setting.rb +5 -1
- data/lib/gooddata/models/user_filters/user_filter_builder.rb +9 -0
- data/lib/gooddata/rest/connection.rb +4 -1
- data/lib/gooddata/version.rb +1 -1
- data/spec/environment/development.rb +29 -0
- data/spec/environment/environment.rb +14 -2
- data/spec/environment/{hotfix.rb → testing.rb} +0 -0
- data/spec/integration/date_dim_switch_spec.rb +3 -5
- data/spec/integration/lcm_spec.rb +24 -21
- data/spec/integration/project_spec.rb +16 -0
- data/spec/integration/segments_spec.rb +1 -1
- data/spec/unit/helpers/global_helpers_spec.rb +26 -2
- data/spec/unit/helpers_spec.rb +20 -0
- data/spec/unit/models/project_creator_spec.rb +3 -2
- metadata +29 -12
- data/lib/gooddata/lcm/actions/ensure_titles.rb +0 -54
- 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 |
|
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
|
|
data/lib/gooddata/lcm/lcm.rb
CHANGED
@@ -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
|
-
|
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 |
|
261
|
-
|
262
|
-
next unless
|
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
|
-
|
279
|
+
attribute = GoodData::MdObject[attr_uri, project: target, client: target.client]
|
265
280
|
|
266
|
-
if
|
267
|
-
|
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 #{
|
287
|
+
GoodData.logger.info "Updating drill path of #{attr_identifier} -> #{drill_path} in '#{target.title}'"
|
270
288
|
end
|
271
289
|
|
272
|
-
|
273
|
-
|
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 #{
|
295
|
+
GoodData.logger.warn "Unable to find #{attr_identifier} in '#{target.title}'"
|
278
296
|
end
|
279
297
|
end
|
280
298
|
|
data/lib/gooddata/lcm/lcm2.rb
CHANGED
@@ -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
|
-
|
20
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
106
|
-
|
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
|
144
|
+
res[k] = convert_to_smart_hash(v)
|
126
145
|
else
|
127
|
-
res[k
|
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
|
-
|
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
|
-
|
265
|
+
errors = []
|
266
|
+
results = []
|
267
|
+
actions.each do |action|
|
215
268
|
puts
|
216
269
|
|
217
270
|
# Invoke action
|
218
|
-
|
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
|
-
#
|
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.
|
311
|
+
brick_results[action.short_name] = results[index]
|
248
312
|
end
|
249
313
|
|
250
314
|
{
|
251
|
-
actions: actions.map
|
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
|