gooddata 2.1.9 → 2.1.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gdc-ii-config.yaml +1 -1
  3. data/.rubocop.yml +1 -0
  4. data/.travis.yml +1 -3
  5. data/CHANGELOG.md +49 -0
  6. data/Dockerfile +17 -7
  7. data/README.md +17 -0
  8. data/SDK_VERSION +1 -1
  9. data/VERSION +1 -1
  10. data/bin/run_brick.rb +3 -0
  11. data/bin/test_projects_cleanup.rb +6 -2
  12. data/ci/bigquery/pom.xml +54 -0
  13. data/ci/redshift/pom.xml +73 -0
  14. data/ci/snowflake/pom.xml +57 -0
  15. data/dev-gooddata-sso.pub.encrypted +40 -40
  16. data/gdc_fossa_lcm.yaml +2 -0
  17. data/gdc_fossa_ruby_sdk.yaml +4 -0
  18. data/gooddata.gemspec +3 -3
  19. data/k8s/charts/lcm-bricks/Chart.yaml +1 -1
  20. data/k8s/charts/lcm-bricks/templates/prometheus/alertingRules.yaml +22 -12
  21. data/lcm.rake +10 -6
  22. data/lib/gooddata/cloud_resources/bigquery/bigquery_client.rb +86 -0
  23. data/lib/gooddata/cloud_resources/bigquery/drivers/.gitkeepme +0 -0
  24. data/lib/gooddata/cloud_resources/redshift/redshift_client.rb +3 -2
  25. data/lib/gooddata/cloud_resources/snowflake/drivers/.gitkeepme +0 -0
  26. data/lib/gooddata/cloud_resources/snowflake/snowflake_client.rb +84 -0
  27. data/lib/gooddata/helpers/data_helper.rb +1 -1
  28. data/lib/gooddata/helpers/data_source_helpers.rb +47 -0
  29. data/lib/gooddata/helpers/global_helpers_params.rb +2 -2
  30. data/lib/gooddata/lcm/actions/collect_clients.rb +6 -6
  31. data/lib/gooddata/lcm/actions/collect_dynamic_schedule_params.rb +6 -6
  32. data/lib/gooddata/lcm/actions/collect_tagged_objects.rb +2 -1
  33. data/lib/gooddata/lcm/actions/collect_users_brick_users.rb +7 -6
  34. data/lib/gooddata/lcm/actions/migrate_gdc_date_dimension.rb +116 -0
  35. data/lib/gooddata/lcm/actions/set_master_project.rb +76 -0
  36. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +10 -1
  37. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +2 -2
  38. data/lib/gooddata/lcm/actions/synchronize_users.rb +31 -30
  39. data/lib/gooddata/lcm/lcm2.rb +22 -1
  40. data/lib/gooddata/models/domain.rb +17 -15
  41. data/lib/gooddata/models/from_wire.rb +1 -0
  42. data/lib/gooddata/models/metadata/scheduled_mail.rb +1 -1
  43. data/lib/gooddata/models/process.rb +11 -3
  44. data/lib/gooddata/models/project.rb +118 -29
  45. data/rubydev_public.gpg.encrypted +51 -51
  46. data/rubydev_secret_keys.gpg.encrypted +109 -109
  47. metadata +23 -12
@@ -50,7 +50,8 @@ module GoodData
50
50
  if transfer_all
51
51
  vizs = MdObject.query('visualizationObject', MdObject, client: development_client, project: from_project)
52
52
  viz_widgets = MdObject.query('visualizationWidget', MdObject, client: development_client, project: from_project)
53
- objects = (from_project.reports.to_a + from_project.metrics.to_a + from_project.variables.to_a + vizs.to_a + viz_widgets.to_a).map(&:uri)
53
+ theme = MdObject.query('theme', MdObject, client: development_client, project: from_project)
54
+ objects = (from_project.reports.to_a + from_project.metrics.to_a + from_project.variables.to_a + vizs.to_a + viz_widgets.to_a + theme.to_a).map(&:uri)
54
55
  elsif production_tags.any?
55
56
  objects = from_project.find_by_tag(production_tags)
56
57
  end
@@ -35,7 +35,7 @@ module GoodData
35
35
  class << self
36
36
  def call(params)
37
37
  users_brick_users = []
38
- login_column = params.users_brick_config.login_column || 'login'
38
+ login_column = params.users_brick_config.login_column&.downcase || 'login'
39
39
  users_brick_data_source = GoodData::Helpers::DataSource.new(params.users_brick_config.input_source)
40
40
 
41
41
  users_brick_data_source_file = without_check(PARAMS, params) do
@@ -45,14 +45,15 @@ module GoodData
45
45
  )
46
46
  end
47
47
  CSV.foreach(users_brick_data_source_file,
48
- headers: true,
49
- return_headers: false,
50
- encoding: 'utf-8') do |row|
51
- pid = row[params.multiple_projects_column]
48
+ :headers => true,
49
+ :return_headers => false,
50
+ :header_converters => :downcase,
51
+ :encoding => 'utf-8') do |row|
52
+ pid = row[params.multiple_projects_column&.downcase]
52
53
  fail "The set multiple_projects_column '#{params.multiple_projects_column}' of the users input is empty" if !pid && MULTIPLE_COLUMN_MODES.include?(params.sync_mode)
53
54
 
54
55
  users_brick_users << {
55
- login: row[login_column].downcase,
56
+ login: row[login_column].nil? ? nil : row[login_column].strip.downcase,
56
57
  pid: pid
57
58
  }
58
59
  end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+ # (C) 2019-2020 GoodData Corporation
3
+ require_relative 'base_action'
4
+
5
+ # Migrate date dimension urn:gooddata:date or urn:custom:date to urn:custom_v2:date
6
+ module GoodData
7
+ module LCM2
8
+ class MigrateGdcDateDimension < BaseAction
9
+ DESCRIPTION = 'Migrate Gdc Date Dimension'
10
+ DATE_DIMENSION_CUSTOM_V2 = 'urn:custom_v2:date'
11
+ DATE_DIMENSION_OLD = %w[urn:gooddata:date urn:custom:date]
12
+
13
+ PARAMS = define_params(self) do
14
+ description 'Client Used for Connecting to GD'
15
+ param :gdc_gd_client, instance_of(Type::GdClientType), required: true
16
+
17
+ description 'Specifies how to synchronize LDM and resolve possible conflicts'
18
+ param :synchronize_ldm, instance_of(Type::SynchronizeLDM), required: false, default: 'diff_against_master_with_fallback'
19
+
20
+ description 'Synchronization Info'
21
+ param :synchronize, array_of(instance_of(Type::SynchronizationInfoType)), required: true, generated: true
22
+ end
23
+
24
+ RESULT_HEADER = %i[from to status]
25
+
26
+ class << self
27
+ def call(params)
28
+ results = []
29
+ params.synchronize.map do |segment_info|
30
+ result = migrate_date_dimension(params, segment_info)
31
+ results.concat(result)
32
+ end
33
+
34
+ {
35
+ results: results
36
+ }
37
+ end
38
+
39
+ def migrate_date_dimension(params, segment_info)
40
+ results = []
41
+ client = params.gdc_gd_client
42
+ latest_blueprint = segment_info[:from_blueprint]
43
+ # don't migrate when latest master doesn't contain custom v2 date.
44
+ return results unless contain_v2?(latest_blueprint)
45
+
46
+ previous_blueprint = segment_info[:previous_master]&.blueprint
47
+ # check latest master and previous master
48
+ master_upgrade_datasets = get_upgrade_dates(latest_blueprint, previous_blueprint) if params[:synchronize_ldm].downcase == 'diff_against_master' && previous_blueprint
49
+ unless master_upgrade_datasets&.empty?
50
+ segment_info[:to].pmap do |entry|
51
+ pid = entry[:pid]
52
+ to_project = client.projects(pid) || fail("Invalid 'to' project specified - '#{pid}'")
53
+ to_blueprint = to_project.blueprint
54
+ upgrade_datasets = get_upgrade_dates(latest_blueprint, to_blueprint)
55
+ next if upgrade_datasets.empty?
56
+
57
+ message = get_upgrade_message(upgrade_datasets)
58
+
59
+ results << {
60
+ from: segment_info[:from],
61
+ to: pid,
62
+ status: to_project.upgrade_custom_v2(message)
63
+ }
64
+ end
65
+ end
66
+
67
+ results
68
+ end
69
+
70
+ def get_upgrade_dates(src_blueprint, dest_blueprint)
71
+ dest_dates = get_date_dimensions(dest_blueprint) if dest_blueprint
72
+ src_dates = get_date_dimensions(src_blueprint) if src_blueprint
73
+
74
+ return false if dest_dates.empty? || src_dates.empty?
75
+
76
+ upgrade_datasets = []
77
+ dest_dates.each do |dest|
78
+ src_dim = get_date_dimension(src_blueprint, dest[:id])
79
+ next unless src_dim
80
+
81
+ upgrade_datasets << src_dim[:identifier] if upgrade?(src_dim, dest) && src_dim[:identifier]
82
+ end
83
+
84
+ upgrade_datasets
85
+ end
86
+
87
+ def get_upgrade_message(upgrade_datasets)
88
+ {
89
+ upgrade: {
90
+ dateDatasets: {
91
+ upgrade: "exact",
92
+ datasets: upgrade_datasets
93
+ }
94
+ }
95
+ }
96
+ end
97
+
98
+ def upgrade?(src_dim, dest_dim)
99
+ src_dim[:urn] == DATE_DIMENSION_CUSTOM_V2 && DATE_DIMENSION_OLD.any? { |e| dest_dim[:urn] == e }
100
+ end
101
+
102
+ def contain_v2?(blueprint)
103
+ get_date_dimensions(blueprint).any? { |e| e[:urn] == DATE_DIMENSION_CUSTOM_V2 }
104
+ end
105
+
106
+ def get_date_dimension(blueprint, id)
107
+ GoodData::Model::ProjectBlueprint.find_date_dimension(blueprint, id)
108
+ end
109
+
110
+ def get_date_dimensions(blueprint)
111
+ GoodData::Model::ProjectBlueprint.date_dimensions(blueprint)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,76 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+ #
4
+ # Copyright (c) 2010-2017 GoodData Corporation. All rights reserved.
5
+ # This source code is licensed under the BSD-style license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+
8
+ require_relative 'base_action'
9
+
10
+ module GoodData
11
+ module LCM2
12
+ class SetMasterProject < BaseAction
13
+ DESCRIPTION = 'Set master project'
14
+
15
+ PARAMS = define_params(self) do
16
+ description 'Organization Name'
17
+ param :organization, instance_of(Type::StringType), required: false
18
+
19
+ description 'Domain'
20
+ param :domain, instance_of(Type::StringType), required: false
21
+
22
+ description 'ADS Client'
23
+ param :ads_client, instance_of(Type::AdsClientType), required: false
24
+
25
+ description 'Table Name'
26
+ param :release_table_name, instance_of(Type::StringType), required: false
27
+
28
+ description 'Segments to manage'
29
+ param :segments, array_of(instance_of(Type::SegmentType)), required: true
30
+
31
+ description 'DataProduct to manage'
32
+ param :data_product, instance_of(Type::GDDataProductType), required: false
33
+
34
+ description 'Released master project should be used in next rollout'
35
+ param :set_master_project, instance_of(Type::StringType), required: false
36
+ end
37
+
38
+ class << self
39
+ def call(params)
40
+ results = []
41
+ domain_name = params.organization || params.domain
42
+ data_product = params.data_product
43
+ params.segments.each do |segment_in|
44
+ version = get_latest_version(params, domain_name, data_product.data_product_id, segment_in.segment_id) + 1
45
+ segment_in[:data_product_id] = data_product.data_product_id
46
+ segment_in[:master_pid] = params.set_master_project
47
+ segment_in[:version] = version
48
+ segment_in[:timestamp] = Time.now.utc.iso8601
49
+
50
+ results << {
51
+ data_product_id: data_product.data_product_id,
52
+ segment_id: segment_in.segment_id,
53
+ version: version
54
+ }
55
+ end
56
+ results
57
+ end
58
+
59
+ def get_latest_version(params, domain_name, data_product_id, segment_id)
60
+ if params.ads_client
61
+ current_master = GoodData::LCM2::Helpers.latest_master_project_from_ads(
62
+ params.release_table_name,
63
+ params.ads_client,
64
+ segment_id
65
+ )
66
+ else
67
+ current_master = GoodData::LCM2::Helpers.latest_master_project_from_nfs(domain_name, data_product_id, segment_id)
68
+ end
69
+ return 0 unless current_master
70
+
71
+ current_master[:version].to_i
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -49,6 +49,8 @@ module GoodData
49
49
  param :include_deprecated, instance_of(Type::BooleanType), required: false, default: false
50
50
  end
51
51
 
52
+ RESULT_HEADER = %i[from to status]
53
+
52
54
  class << self
53
55
  def call(params)
54
56
  results = []
@@ -76,9 +78,9 @@ module GoodData
76
78
  include_deprecated = params.include_deprecated.to_b
77
79
  from_pid = segment_info[:from]
78
80
  from = params.development_client.projects(from_pid) || fail("Invalid 'from' project specified - '#{from_pid}'")
79
-
80
81
  GoodData.logger.info "Creating Blueprint, project: '#{from.title}', PID: #{from_pid}"
81
82
  blueprint = from.blueprint(include_ca: params.include_computed_attributes.to_b)
83
+ segment_info[:from_blueprint] = blueprint
82
84
  maql_diff = nil
83
85
  previous_master = segment_info[:previous_master]
84
86
  diff_against_master = %w(diff_against_master_with_fallback diff_against_master)
@@ -89,6 +91,13 @@ module GoodData
89
91
  maql_diff_params << :excludeFactRule if exclude_fact_rule
90
92
  maql_diff_params << :includeDeprecated if include_deprecated
91
93
  maql_diff = previous_master.maql_diff(blueprint: blueprint, params: maql_diff_params)
94
+ chunks = maql_diff['projectModelDiff']['updateScripts']
95
+ if chunks.empty?
96
+ GoodData.logger.info "Synchronize LDM to clients will not proceed in mode \
97
+ '#{params[:synchronize_ldm].downcase}' due to no LDM changes in the new master project. \
98
+ If you had changed LDM of clients manually, please use mode 'diff_against_clients' \
99
+ to force synchronize LDM to clients"
100
+ end
92
101
  end
93
102
 
94
103
  segment_info[:to] = segment_info[:to].pmap do |entry|
@@ -230,8 +230,8 @@ module GoodData
230
230
  begin
231
231
  GoodData.logger.info('Start reading data')
232
232
  row_count = 0
233
- CSV.foreach(tmp, headers: csv_with_headers, return_headers: false, encoding: 'utf-8') do |row|
234
- filters << row.to_hash.merge(pid: row[multiple_projects_column])
233
+ CSV.foreach(tmp, :headers => csv_with_headers, :return_headers => false, :header_converters => :downcase, :encoding => 'utf-8') do |row|
234
+ filters << row.to_hash.merge(pid: row[multiple_projects_column.downcase])
235
235
  row_count += 1
236
236
  GoodData.logger.info("Read #{row_count} rows") if (row_count % 50_000).zero?
237
237
  end
@@ -347,38 +347,33 @@ module GoodData
347
347
  end
348
348
 
349
349
  def load_data(params, data_source)
350
- first_name_column = params.first_name_column || 'first_name'
351
- last_name_column = params.last_name_column || 'last_name'
352
- login_column = params.login_column || 'login'
353
- password_column = params.password_column || 'password'
354
- email_column = params.email_column || 'email'
355
- role_column = params.role_column || 'role'
356
- sso_provider_column = params.sso_provider_column || 'sso_provider'
357
- authentication_modes_column = params.authentication_modes_column || 'authentication_modes'
358
- user_groups_column = params.user_groups_column || 'user_groups'
359
- language_column = params.language_column || 'language'
360
- company_column = params.company_column || 'company'
361
- position_column = params.position_column || 'position'
362
- country_column = params.country_column || 'country'
363
- phone_column = params.phone_column || 'phone'
364
- ip_whitelist_column = params.ip_whitelist_column || 'ip_whitelist'
350
+ first_name_column = params.first_name_column&.downcase || 'first_name'
351
+ last_name_column = params.last_name_column&.downcase || 'last_name'
352
+ login_column = params.login_column&.downcase || 'login'
353
+ password_column = params.password_column&.downcase || 'password'
354
+ email_column = params.email_column&.downcase || 'email'
355
+ role_column = params.role_column&.downcase || 'role'
356
+ sso_provider_column = params.sso_provider_column&.downcase || 'sso_provider'
357
+ authentication_modes_column = params.authentication_modes_column&.downcase || 'authentication_modes'
358
+ user_groups_column = params.user_groups_column&.downcase || 'user_groups'
359
+ language_column = params.language_column&.downcase || 'language'
360
+ company_column = params.company_column&.downcase || 'company'
361
+ position_column = params.position_column&.downcase || 'position'
362
+ country_column = params.country_column&.downcase || 'country'
363
+ phone_column = params.phone_column&.downcase || 'phone'
364
+ ip_whitelist_column = params.ip_whitelist_column&.downcase || 'ip_whitelist'
365
365
 
366
366
  sso_provider = params.sso_provider
367
367
  authentication_modes = params.authentication_modes || []
368
368
 
369
- dwh = params.ads_client
370
- if dwh
371
- data = dwh.execute_select(params.input_source.query)
372
- else
373
- tmp = without_check(PARAMS, params) do
374
- File.open(data_source.realize(params), 'r:UTF-8')
375
- end
369
+ tmp = without_check(PARAMS, params) do
370
+ File.open(data_source.realize(params), 'r:UTF-8')
371
+ end
376
372
 
377
- begin
378
- data = read_csv_file(tmp)
379
- rescue Exception => e # rubocop:disable RescueException
380
- fail "There was an error during loading users from csv file. Message: #{e.message}. Error: #{e}"
381
- end
373
+ begin
374
+ data = read_csv_file(tmp)
375
+ rescue Exception => e # rubocop:disable RescueException
376
+ fail "There was an error during loading users from csv file. Message: #{e.message}. Error: #{e}"
382
377
  end
383
378
 
384
379
  data.map do |row|
@@ -398,12 +393,18 @@ module GoodData
398
393
  ip_whitelist = row[ip_whitelist_column] || row[ip_whitelist_column.to_sym]
399
394
  ip_whitelist = ip_whitelist.split(',').map(&:strip) if ip_whitelist
400
395
 
396
+ user_login = row[login_column] || row[login_column.to_sym]
397
+ user_login = user_login.strip unless user_login.nil?
398
+
399
+ user_email = row[email_column] || row[login_column] || row[email_column.to_sym] || row[login_column.to_sym]
400
+ user_email = user_email.strip unless user_email.nil?
401
+
401
402
  {
402
403
  :first_name => row[first_name_column] || row[first_name_column.to_sym],
403
404
  :last_name => row[last_name_column] || row[last_name_column.to_sym],
404
- :login => row[login_column] || row[login_column.to_sym],
405
+ :login => user_login,
405
406
  :password => row[password_column] || row[password_column.to_sym],
406
- :email => row[email_column] || row[login_column] || row[email_column.to_sym] || row[login_column.to_sym],
407
+ :email => user_email,
407
408
  :role => row[role_column] || row[role_column.to_sym],
408
409
  :sso_provider => sso_provider || row[sso_provider_column] || row[sso_provider_column.to_sym],
409
410
  :authentication_modes => modes,
@@ -424,7 +425,7 @@ module GoodData
424
425
  res = []
425
426
  row_count = 0
426
427
 
427
- CSV.foreach(path, :headers => true) do |row|
428
+ CSV.foreach(path, :headers => true, :header_converters => :downcase, :encoding => 'utf-8') do |row|
428
429
  if block_given?
429
430
  data = yield row
430
431
  else
@@ -106,6 +106,14 @@ module GoodData
106
106
  UpdateReleaseTable
107
107
  ],
108
108
 
109
+ release_set_master_project: [
110
+ EnsureReleaseTable,
111
+ CollectDataProduct,
112
+ SegmentsFilter,
113
+ SetMasterProject,
114
+ UpdateReleaseTable
115
+ ],
116
+
109
117
  provision: [
110
118
  EnsureReleaseTable,
111
119
  CollectDataProduct,
@@ -130,6 +138,7 @@ module GoodData
130
138
  EnsureTechnicalUsersDomain,
131
139
  EnsureTechnicalUsersProject,
132
140
  SynchronizeLdm,
141
+ MigrateGdcDateDimension,
133
142
  SynchronizeClients,
134
143
  SynchronizeComputedAttributes,
135
144
  CollectDymanicScheduleParams,
@@ -271,8 +280,14 @@ module GoodData
271
280
 
272
281
  GoodData.gd_logger.brick = mode
273
282
 
283
+ final_mode = if params.set_master_project && mode == 'release'
284
+ 'release_set_master_project'
285
+ else
286
+ mode
287
+ end
288
+
274
289
  # Get actions for mode specified
275
- actions = get_mode_actions(mode)
290
+ actions = get_mode_actions(final_mode)
276
291
 
277
292
  if params.actions
278
293
  actions = params.actions.map do |action|
@@ -305,6 +320,12 @@ module GoodData
305
320
  skip_actions.include?(action.name.split('::').last)
306
321
  end
307
322
 
323
+ sync_mode = params.fetch(:sync_mode, nil)
324
+ if mode == 'users' && %w[add_to_organization remove_from_organization].include?(sync_mode)
325
+ actions = actions.reject do |action|
326
+ %w[CollectDataProduct CollectSegments].include?(action.name.split('::').last)
327
+ end
328
+ end
308
329
  check_unused_params(actions, params)
309
330
  print_action_names(mode, actions)
310
331
 
@@ -222,24 +222,26 @@ module GoodData
222
222
  domain = client.domain(domain)
223
223
  if id == :all
224
224
  GoodData.logger.warn("Retrieving all users from domain #{domain.name}")
225
- Enumerator.new do |y|
226
- page_limit = opts[:page_limit] || 1000
227
- offset = opts[:offset] || 0
228
- loop do
229
- begin
230
- tmp = client(opts).get("#{domain.uri}/users", params: { offset: offset, limit: page_limit })
231
- end
232
-
233
- tmp['accountSettings']['items'].each do |user_data|
234
- user = client.create(GoodData::Profile, user_data)
235
- y << user if user
236
- end
237
- break if tmp['accountSettings']['items'].count < page_limit
238
- offset += page_limit
225
+ all_users = []
226
+ page_limit = opts[:page_limit] || 1000
227
+ offset = opts[:offset] || 0
228
+ loop do
229
+ begin
230
+ tmp = client(opts).get("#{domain.uri}/users", params: { offset: offset, limit: page_limit })
239
231
  end
232
+
233
+ tmp['accountSettings']['items'].each do |user_data|
234
+ user = client.create(GoodData::Profile, user_data)
235
+ all_users << user if user
236
+ end
237
+ break if tmp['accountSettings']['items'].count < page_limit
238
+
239
+ offset += page_limit
240
240
  end
241
+
242
+ all_users
241
243
  else
242
- find_user_by_login(domain, id)
244
+ find_user_by_login(domain, id, opts)
243
245
  end
244
246
  end
245
247