gooddata 2.2.0 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/.gdc-ii-config.yaml +42 -1
  3. data/.github/workflows/build.yml +14 -13
  4. data/.github/workflows/pre-merge.yml +13 -13
  5. data/.pronto.yml +1 -0
  6. data/.rubocop.yml +2 -14
  7. data/CHANGELOG.md +9 -0
  8. data/Dockerfile +13 -7
  9. data/Dockerfile.jruby +5 -5
  10. data/Dockerfile.ruby +5 -7
  11. data/Gemfile +4 -2
  12. data/README.md +5 -4
  13. data/Rakefile +1 -1
  14. data/SDK_VERSION +1 -1
  15. data/VERSION +1 -1
  16. data/bin/run_brick.rb +7 -0
  17. data/ci/mysql/pom.xml +6 -1
  18. data/ci/redshift/pom.xml +3 -4
  19. data/docker-compose.lcm.yml +42 -1
  20. data/docker-compose.yml +42 -0
  21. data/gooddata.gemspec +21 -22
  22. data/lcm.rake +9 -0
  23. data/lib/gooddata/bricks/base_pipeline.rb +26 -0
  24. data/lib/gooddata/bricks/brick.rb +0 -1
  25. data/lib/gooddata/bricks/middleware/execution_result_middleware.rb +3 -3
  26. data/lib/gooddata/bricks/pipeline.rb +2 -14
  27. data/lib/gooddata/cloud_resources/mysql/mysql_client.rb +18 -8
  28. data/lib/gooddata/cloud_resources/redshift/drivers/.gitkeepme +0 -0
  29. data/lib/gooddata/cloud_resources/redshift/redshift_client.rb +0 -2
  30. data/lib/gooddata/cloud_resources/snowflake/snowflake_client.rb +1 -1
  31. data/lib/gooddata/lcm/actions/base_action.rb +157 -0
  32. data/lib/gooddata/lcm/actions/collect_data_product.rb +2 -1
  33. data/lib/gooddata/lcm/actions/collect_projects_warning_status.rb +53 -0
  34. data/lib/gooddata/lcm/actions/collect_segment_clients.rb +14 -0
  35. data/lib/gooddata/lcm/actions/initialize_continue_on_error_option.rb +87 -0
  36. data/lib/gooddata/lcm/actions/migrate_gdc_date_dimension.rb +28 -2
  37. data/lib/gooddata/lcm/actions/provision_clients.rb +34 -5
  38. data/lib/gooddata/lcm/actions/synchronize_cas.rb +24 -4
  39. data/lib/gooddata/lcm/actions/synchronize_clients.rb +56 -4
  40. data/lib/gooddata/lcm/actions/synchronize_dataset_mappings.rb +28 -3
  41. data/lib/gooddata/lcm/actions/synchronize_etls_in_segment.rb +48 -11
  42. data/lib/gooddata/lcm/actions/synchronize_kd_dashboard_permission.rb +103 -0
  43. data/lib/gooddata/lcm/actions/synchronize_ldm.rb +60 -15
  44. data/lib/gooddata/lcm/actions/synchronize_ldm_layout.rb +98 -0
  45. data/lib/gooddata/lcm/actions/synchronize_pp_dashboard_permission.rb +108 -0
  46. data/lib/gooddata/lcm/actions/synchronize_schedules.rb +31 -1
  47. data/lib/gooddata/lcm/actions/synchronize_user_filters.rb +14 -9
  48. data/lib/gooddata/lcm/actions/synchronize_user_groups.rb +30 -4
  49. data/lib/gooddata/lcm/actions/synchronize_users.rb +11 -10
  50. data/lib/gooddata/lcm/actions/update_metric_formats.rb +21 -4
  51. data/lib/gooddata/lcm/exceptions/lcm_execution_warning.rb +15 -0
  52. data/lib/gooddata/lcm/helpers/check_helper.rb +19 -0
  53. data/lib/gooddata/lcm/lcm2.rb +45 -4
  54. data/lib/gooddata/lcm/user_bricks_helper.rb +9 -0
  55. data/lib/gooddata/mixins/inspector.rb +1 -1
  56. data/lib/gooddata/models/ldm_layout.rb +38 -0
  57. data/lib/gooddata/models/project.rb +197 -22
  58. data/lib/gooddata/models/project_creator.rb +83 -6
  59. data/lib/gooddata/models/segment.rb +2 -1
  60. data/lib/gooddata/models/user_filters/user_filter_builder.rb +104 -15
  61. data/lib/gooddata/rest/connection.rb +5 -3
  62. data/lib/gooddata/rest/phmap.rb +1 -0
  63. data/lib/gooddata.rb +1 -0
  64. data/lib/gooddata_brick_base.rb +35 -0
  65. data/sonar-project.properties +6 -0
  66. metadata +64 -59
  67. data/lib/gooddata/cloud_resources/redshift/drivers/log4j.properties +0 -15
@@ -35,14 +35,26 @@ module GoodData
35
35
 
36
36
  description 'Localization query'
37
37
  param :localization_query, instance_of(Type::StringType), required: false
38
+
39
+ description 'Abort on error'
40
+ param :abort_on_error, instance_of(Type::StringType), required: false
41
+
42
+ description 'Collect synced status'
43
+ param :collect_synced_status, instance_of(Type::BooleanType), required: false
44
+
45
+ description 'Sync failed list'
46
+ param :sync_failed_list, instance_of(Type::HashType), required: false
38
47
  end
39
48
 
40
49
  RESULT_HEADER = %i[action ok_clients error_clients]
41
50
 
42
51
  class << self
43
52
  def load_metric_data(params)
53
+ collect_synced_status = collect_synced_status(params)
54
+
44
55
  if params&.dig(:input_source, :metric_format) && params[:input_source][:metric_format].present?
45
- metric_input_source = validate_input_source(params[:input_source])
56
+ metric_input_source = validate_input_source(params[:input_source], collect_synced_status)
57
+ return nil unless metric_input_source
46
58
  else
47
59
  return nil
48
60
  end
@@ -68,10 +80,14 @@ module GoodData
68
80
  metrics_hash
69
81
  end
70
82
 
71
- def validate_input_source(input_source)
83
+ def validate_input_source(input_source, continue_on_error)
72
84
  type = input_source[:type] if input_source&.dig(:type)
73
85
  metric_format = input_source[:metric_format]
74
- raise "Incorrect configuration: 'type' of 'input_source' is required" if type.blank?
86
+ if type.blank?
87
+ raise "Incorrect configuration: 'type' of 'input_source' is required" unless continue_on_error
88
+
89
+ return nil
90
+ end
75
91
 
76
92
  modified_input_source = input_source
77
93
  case type
@@ -154,8 +170,9 @@ module GoodData
154
170
  data_product_clients = data_product.clients
155
171
  number_client_ok = 0
156
172
  number_client_error = 0
173
+ collect_synced_status = collect_synced_status(params)
157
174
  metric_group.each do |client_id, formats|
158
- next unless updated_clients.include?(client_id)
175
+ next if !updated_clients.include?(client_id) || (collect_synced_status && sync_failed_client(client_id, params))
159
176
 
160
177
  client = data_product_clients.find { |c| c.id == client_id }
161
178
  begin
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ # (C) 2019-2022 GoodData Corporation
3
+
4
+ module GoodData
5
+ class LcmExecutionWarning < RuntimeError
6
+ DEFAULT_MSG = 'Existing errors during lcm execution'
7
+
8
+ attr_reader :summary_error
9
+
10
+ def initialize(summary_error, message = DEFAULT_MSG)
11
+ super(message)
12
+ @summary_error = summary_error
13
+ end
14
+ end
15
+ end
@@ -10,6 +10,9 @@ module GoodData
10
10
  module LCM2
11
11
  class Helpers
12
12
  class << self
13
+ ABORT_ON_ERROR_PARAM = 'abort_on_error'.to_sym
14
+ COLLECT_SYNCED_STATUS = 'collect_synced_status'.to_sym
15
+
13
16
  def check_params(specification, params)
14
17
  specification.keys.each do |param_name|
15
18
  value = params.send(param_name)
@@ -39,6 +42,22 @@ module GoodData
39
42
  end
40
43
  end
41
44
  end
45
+
46
+ def continue_on_error(params)
47
+ params.include?(ABORT_ON_ERROR_PARAM) && !to_bool(ABORT_ON_ERROR_PARAM, params[ABORT_ON_ERROR_PARAM])
48
+ end
49
+
50
+ def collect_synced_status(params)
51
+ params.include?(COLLECT_SYNCED_STATUS) && to_bool(COLLECT_SYNCED_STATUS, params[COLLECT_SYNCED_STATUS])
52
+ end
53
+
54
+ def to_bool(key, value)
55
+ return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
56
+ return true if value =~ /^(true|t|yes|y|1)$/i
57
+ return false if value == '' || value =~ /^(false|f|no|n|0)$/i
58
+
59
+ raise ArgumentError, "Invalid '#{value}' boolean value for '#{key}' parameter"
60
+ end
42
61
  end
43
62
  end
44
63
  end
@@ -17,6 +17,7 @@ require_relative 'actions/actions'
17
17
  require_relative 'dsl/dsl'
18
18
  require_relative 'helpers/helpers'
19
19
  require_relative 'exceptions/lcm_execution_error'
20
+ require_relative 'exceptions/lcm_execution_warning'
20
21
 
21
22
  using TrueExtensions
22
23
  using FalseExtensions
@@ -97,10 +98,13 @@ module GoodData
97
98
  ImportObjectCollections,
98
99
  SynchronizeComputedAttributes,
99
100
  SynchronizeDataSetMapping,
101
+ SynchronizeLdmLayout,
100
102
  SynchronizeProcesses,
101
103
  SynchronizeSchedules,
102
104
  SynchronizeColorPalette,
103
105
  SynchronizeUserGroups,
106
+ SynchronizePPDashboardPermissions,
107
+ SynchronizeKDDashboardPermissions,
104
108
  SynchronizeNewSegments,
105
109
  UpdateReleaseTable
106
110
  ],
@@ -122,13 +126,19 @@ module GoodData
122
126
  CollectClients,
123
127
  AssociateClients,
124
128
  RenameExistingClientProjects,
129
+ InitializeContinueOnErrorOption,
125
130
  ProvisionClients,
126
131
  UpdateMetricFormats,
127
132
  EnsureTechnicalUsersDomain,
128
133
  EnsureTechnicalUsersProject,
129
134
  CollectDymanicScheduleParams,
130
135
  SynchronizeDataSetMapping,
131
- SynchronizeETLsInSegment
136
+ SynchronizeLdmLayout,
137
+ SynchronizeUserGroups,
138
+ SynchronizePPDashboardPermissions,
139
+ SynchronizeKDDashboardPermissions,
140
+ SynchronizeETLsInSegment,
141
+ CollectProjectsWarningStatus
132
142
  ],
133
143
 
134
144
  rollout: [
@@ -138,14 +148,20 @@ module GoodData
138
148
  CollectSegmentClients,
139
149
  EnsureTechnicalUsersDomain,
140
150
  EnsureTechnicalUsersProject,
151
+ InitializeContinueOnErrorOption,
141
152
  SynchronizeLdm,
142
153
  SynchronizeDataSetMapping,
154
+ SynchronizeLdmLayout,
143
155
  MigrateGdcDateDimension,
144
156
  SynchronizeClients,
157
+ SynchronizeUserGroups,
158
+ SynchronizePPDashboardPermissions,
159
+ SynchronizeKDDashboardPermissions,
145
160
  UpdateMetricFormats,
146
161
  SynchronizeComputedAttributes,
147
162
  CollectDymanicScheduleParams,
148
- SynchronizeETLsInSegment
163
+ SynchronizeETLsInSegment,
164
+ CollectProjectsWarningStatus
149
165
  ],
150
166
 
151
167
  users: [
@@ -340,7 +356,7 @@ module GoodData
340
356
  # Invoke action
341
357
  begin
342
358
  out = run_action action, params
343
- rescue Exception => e # rubocop:disable RescueException
359
+ rescue Exception => e # rubocop:disable Style/RescueException
344
360
  errors << {
345
361
  action: action,
346
362
  err: e,
@@ -362,7 +378,7 @@ module GoodData
362
378
  params.merge!(new_params)
363
379
 
364
380
  # Print action result
365
- print_action_result(action, res)
381
+ print_action_result(action, res) if action.send(:print_result, params)
366
382
 
367
383
  # Store result for final summary
368
384
  results << res
@@ -389,9 +405,28 @@ module GoodData
389
405
  end
390
406
  end
391
407
 
408
+ process_sync_failed_projects(params) if GoodData::LCM2::Helpers.collect_synced_status(params) && strict_mode
409
+
392
410
  result
393
411
  end
394
412
 
413
+ def process_sync_failed_projects(params)
414
+ sync_failed_list = params[:sync_failed_list]
415
+ sync_project_list = sync_failed_list[:project_client_mappings]
416
+ sync_failed_project_list = sync_failed_list[:failed_detailed_projects]
417
+
418
+ if sync_project_list && sync_failed_project_list && sync_project_list.size.positive? && sync_failed_project_list.size.positive?
419
+ failed_project = sync_failed_project_list[0]
420
+ summary_message = "Existing errors during execution. See log for details"
421
+ error_message = failed_project[:message]
422
+ if sync_project_list.size == sync_failed_project_list.size
423
+ raise GoodData::LcmExecutionError.new(summary_message, error_message)
424
+ else
425
+ raise GoodData::LcmExecutionWarning.new(summary_message, error_message)
426
+ end
427
+ end
428
+ end
429
+
395
430
  def run_action(action, params)
396
431
  begin
397
432
  GoodData.gd_logger.start_action action, GoodData.gd_logger
@@ -401,6 +436,12 @@ module GoodData
401
436
  BaseAction.check_params(action.const_get('PARAMS'), params)
402
437
  params.setup_filters(action.const_get('PARAMS'))
403
438
  out = action.send(:call, params)
439
+ rescue Exception => e # rubocop:disable Style/RescueException
440
+ # Log to splunk
441
+ GoodData.gd_logger.error("action=#{action} status=failed message=#{e} exception=#{e.backtrace}")
442
+ # Log to execution log
443
+ GoodData.logger.error("Execution #{action} failed. Error: #{e}. Detail:#{e.backtrace}")
444
+ raise e
404
445
  ensure
405
446
  params.clear_filters
406
447
  GoodData.gd_logger.end_action GoodData.gd_logger
@@ -26,6 +26,15 @@ module GoodData
26
26
 
27
27
  goodot_id.empty? ? client.id : goodot_id
28
28
  end
29
+
30
+ def non_working_clients(domain_clients, working_client_ids)
31
+ non_working_clients = []
32
+ domain_clients.each do |c|
33
+ non_working_clients << c unless working_client_ids.include?(c.client_id.to_s)
34
+ end
35
+
36
+ non_working_clients
37
+ end
29
38
  end
30
39
  end
31
40
  end
@@ -10,7 +10,7 @@ module GoodData
10
10
  module Mixin
11
11
  # When an RSpec test like this fails,
12
12
  #
13
- # @my_array.should == [@some_model, @some_model2]
13
+ # expect(@my_array).to == [@some_model, @some_model2]
14
14
  #
15
15
  # RSpec will call inspect on each of the objects to "help" you figure out
16
16
  # what went wrong. Well, inspect will usually dump a TON OF SHIT and make trying
@@ -0,0 +1,38 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+ #
4
+ # Copyright (c) 2022 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
+ module GoodData
9
+ class LdmLayout
10
+ DEFAULT_EMPTY_LDM_LAYOUT = {
11
+ "ldmLayout" => {
12
+ "layout" => []
13
+ }
14
+ }
15
+
16
+ LDM_LAYOUT_URI = '/gdc/dataload/internal/projects/%<project_id>s/ldmLayout'
17
+
18
+ class << self
19
+ def get(opts = { :client => GoodData.connection, :project => GoodData.project })
20
+ client, project = GoodData.get_client_and_project(opts)
21
+ get_uri = LDM_LAYOUT_URI % { project_id: project.pid }
22
+
23
+ client.get(get_uri)
24
+ end
25
+ end
26
+
27
+ def initialize(data)
28
+ @data = data
29
+ end
30
+
31
+ def save(opts)
32
+ client, project = GoodData.get_client_and_project(opts)
33
+ post_uri = LDM_LAYOUT_URI % { project_id: project.pid }
34
+
35
+ client.post(post_uri, @data, opts)
36
+ end
37
+ end
38
+ end
@@ -31,6 +31,7 @@ require_relative 'project_log_formatter'
31
31
  require_relative 'project_role'
32
32
  require_relative 'blueprint/blueprint'
33
33
  require_relative 'dataset_mapping'
34
+ require_relative 'ldm_layout'
34
35
 
35
36
  require_relative 'metadata/scheduled_mail'
36
37
  require_relative 'metadata/scheduled_mail/dashboard_attachment'
@@ -272,6 +273,29 @@ module GoodData
272
273
  }
273
274
  end
274
275
 
276
+ def get_ldm_layout(from_project)
277
+ GoodData::LdmLayout.get(:client => from_project.client, :project => from_project)
278
+ rescue StandardError => e
279
+ GoodData.logger.warn "An unexpected error when get ldm layout. Error: #{e.message}"
280
+ GoodData::LdmLayout::DEFAULT_EMPTY_LDM_LAYOUT
281
+ end
282
+
283
+ def save_ldm_layout(ldm_layout_json, to_project)
284
+ ldm_layout = GoodData::LdmLayout.new(ldm_layout_json)
285
+ begin
286
+ ldm_layout.save(:client => to_project.client, :project => to_project)
287
+ status = "OK"
288
+ rescue StandardError => e
289
+ GoodData.logger.warn "An unexpected error when save ldm layout. Error: #{e.message}"
290
+ status = "Failed"
291
+ end
292
+
293
+ {
294
+ to: to_project.pid,
295
+ status: status
296
+ }
297
+ end
298
+
275
299
  # @param from_project The source project
276
300
  # @param to_project The target project
277
301
  # @param options Optional parameters
@@ -416,16 +440,6 @@ module GoodData
416
440
  new_group.project = to_project
417
441
  new_group.description = ug.description
418
442
  new_group.save
419
- # migrate dashboard "grantees"
420
- dashboards = from_project.dashboards
421
- dashboards.each do |dashboard|
422
- new_dashboard = to_project.dashboards.select { |dash| dash.title == dashboard.title }.first
423
- next unless new_dashboard
424
- grantee = dashboard.grantees['granteeURIs']['items'].select { |item| item['aclEntryURI']['grantee'].split('/').last == ug.links['self'].split('/').last }.first
425
- next unless grantee
426
- permission = grantee['aclEntryURI']['permission']
427
- new_dashboard.grant(:member => new_group, :permission => permission)
428
- end
429
443
 
430
444
  {
431
445
  from: from_project.pid,
@@ -436,6 +450,68 @@ module GoodData
436
450
  end
437
451
  end
438
452
 
453
+ def transfer_dashboard_permission(from_project, to_project, source_dashboards, target_dashboards)
454
+ source_user_groups = from_project.user_groups
455
+ target_user_groups = to_project.user_groups
456
+
457
+ source_dashboards.each do |source_dashboard|
458
+ target_dashboard = target_dashboards.select { |dash| dash.title == source_dashboard.title }.first
459
+ next unless target_dashboard
460
+
461
+ begin
462
+ source_group_dashboards = dashboard_user_groups(source_user_groups, source_dashboard)
463
+ target_group_dashboards = dashboard_user_groups(target_user_groups, target_dashboard)
464
+
465
+ common_group_names = source_group_dashboards.flat_map { |s| target_group_dashboards.select { |t| t[:name] == s[:name] } }.map { |x| [x[:name], true] }.to_h
466
+
467
+ remove_user_groups_from_dashboard(target_group_dashboards, target_dashboard, common_group_names)
468
+ add_user_groups_to_dashboard(source_group_dashboards, target_dashboard, common_group_names, target_user_groups)
469
+ rescue StandardError => e
470
+ GoodData.logger.warn "Failed to synchronize dashboard permission from project: '#{from_project.title}', PID: '#{from_project.pid}' to project: '#{to_project.title}', PID: '#{to_project.pid}', dashboard: '#{target_dashboard.title}', dashboard uri: '#{target_dashboard.uri}' . Error: #{e.message} - #{e}" # rubocop:disable Metrics/LineLength
471
+ end
472
+ end
473
+ end
474
+
475
+ def dashboard_user_groups(user_groups, dashboard)
476
+ group_dashboards = []
477
+ dashboard_grantees = dashboard.grantees['granteeURIs']['items'].select { |item| item['aclEntryURI']['grantee'].include?('/usergroups/') }
478
+
479
+ dashboard_grantees.each do |dashboard_grantee|
480
+ permission = dashboard_grantee['aclEntryURI']['permission']
481
+ group_id = dashboard_grantee['aclEntryURI']['grantee'].split('/').last
482
+ user_group = user_groups.select { |group| group.links['self'].split('/').last == group_id }.first
483
+ next unless user_group
484
+
485
+ group_dashboards << {
486
+ name: user_group.name,
487
+ user_group: user_group,
488
+ permission: permission
489
+ }
490
+ end
491
+ group_dashboards
492
+ end
493
+
494
+ def remove_user_groups_from_dashboard(group_dashboards, dashboard, common_group_names)
495
+ group_dashboards.each do |group_dashboard|
496
+ group_name = group_dashboard[:name]
497
+ next if common_group_names && common_group_names[group_name]
498
+
499
+ dashboard.revoke(:member => group_dashboard[:user_group], :permission => group_dashboard[:permission])
500
+ end
501
+ end
502
+
503
+ def add_user_groups_to_dashboard(group_dashboards, dashboard, common_group_names, target_user_groups)
504
+ group_dashboards.each do |group_dashboard|
505
+ group_name = group_dashboard[:name]
506
+ next if common_group_names && common_group_names[group_name]
507
+
508
+ target_user_group = target_user_groups.select { |group| group.name == group_name }.first
509
+ next unless target_user_group
510
+
511
+ dashboard.grant(:member => target_user_group, :permission => group_dashboard[:permission])
512
+ end
513
+ end
514
+
439
515
  # Clones project along with etl and schedules.
440
516
  #
441
517
  # @param client [GoodData::Rest::Client] GoodData client to be used for connection
@@ -443,7 +519,7 @@ module GoodData
443
519
  # Object to be cloned from. Can be either segment in which case we take
444
520
  # the master, client in which case we take its project, string in which
445
521
  # case we treat is as an project object or directly project.
446
- def transfer_schedules(from_project, to_project)
522
+ def transfer_schedules(from_project, to_project, has_cycle_trigger = false)
447
523
  to_project_processes = to_project.processes.sort_by(&:name)
448
524
  from_project_processes = from_project.processes.sort_by(&:name)
449
525
  from_project_processes.reject!(&:add_v2_component?)
@@ -496,15 +572,23 @@ module GoodData
496
572
  end
497
573
 
498
574
  results = []
575
+ update_trigger_schedules = []
499
576
  loop do # rubocop:disable Metrics/BlockLength
500
577
  break if stack.empty?
501
578
  state, changed_schedule = stack.shift
579
+ lazy_update_trigger_info = false
502
580
  if state == :added
503
581
  schedule_spec = changed_schedule
504
582
  if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
505
- stack << [state, schedule_spec]
506
- next
583
+ if has_cycle_trigger
584
+ # The schedule is triggered by another schedule
585
+ lazy_update_trigger_info = true
586
+ else
587
+ stack << [state, schedule_spec]
588
+ next
589
+ end
507
590
  end
591
+
508
592
  remote_process, process_spec = cache.find do |_remote, local, schedule|
509
593
  (schedule_spec[:process_id] == local.process_id) && (schedule.name == schedule_spec[:name])
510
594
  end
@@ -517,8 +601,21 @@ module GoodData
517
601
  if process_spec.type != :dataload
518
602
  executable = schedule_spec[:executable] || (process_spec.type == :ruby ? 'main.rb' : 'main.grf')
519
603
  end
604
+
520
605
  params = schedule_parameters(schedule_spec)
521
- created_schedule = remote_process.create_schedule(schedule_spec[:cron] || schedule_cache[schedule_spec[:after]], executable, params)
606
+
607
+ if lazy_update_trigger_info
608
+ # Temporary update nil for trigger info. The trigger info will be update late after transfer all schedules
609
+ created_schedule = remote_process.create_schedule(nil, executable, params)
610
+ update_trigger_schedules << {
611
+ state: :added,
612
+ schedule: created_schedule,
613
+ after: schedule_spec[:after]
614
+ }
615
+ else
616
+ created_schedule = remote_process.create_schedule(schedule_spec[:cron] || schedule_cache[schedule_spec[:after]], executable, params)
617
+ end
618
+
522
619
  schedule_cache[created_schedule.name] = created_schedule
523
620
 
524
621
  results << {
@@ -529,8 +626,13 @@ module GoodData
529
626
  else
530
627
  schedule_spec = changed_schedule[:new_obj]
531
628
  if schedule_spec[:after] && !schedule_cache[schedule_spec[:after]]
532
- stack << [state, schedule_spec]
533
- next
629
+ if has_cycle_trigger
630
+ # The schedule is triggered by another schedule
631
+ lazy_update_trigger_info = true
632
+ else
633
+ stack << [state, schedule_spec]
634
+ next
635
+ end
534
636
  end
535
637
 
536
638
  remote_process, process_spec = cache.find do |i|
@@ -543,8 +645,12 @@ module GoodData
543
645
 
544
646
  schedule.params = (schedule_spec[:params] || {})
545
647
  schedule.cron = schedule_spec[:cron] if schedule_spec[:cron]
546
- schedule.after = schedule_cache[schedule_spec[:after]] if schedule_spec[:after]
547
- schedule.trigger_execution_status = schedule_cache[schedule_spec[:trigger_execution_status]] if schedule_spec[:after]
648
+
649
+ unless lazy_update_trigger_info
650
+ schedule.after = schedule_cache[schedule_spec[:after]] if schedule_spec[:after]
651
+ schedule.trigger_execution_status = schedule_cache[schedule_spec[:trigger_execution_status]] if schedule_spec[:after]
652
+ end
653
+
548
654
  schedule.hidden_params = schedule_spec[:hidden_params] || {}
549
655
  if process_spec.type != :dataload
550
656
  schedule.executable = schedule_spec[:executable] || (process_spec.type == :ruby ? 'main.rb' : 'main.grf')
@@ -556,6 +662,15 @@ module GoodData
556
662
  schedule.save
557
663
  schedule_cache[schedule.name] = schedule
558
664
 
665
+ if lazy_update_trigger_info
666
+ update_trigger_schedules << {
667
+ state: :changed,
668
+ schedule: schedule,
669
+ after: schedule_spec[:after],
670
+ trigger_execution_status: schedule_spec[:trigger_execution_status]
671
+ }
672
+ end
673
+
559
674
  results << {
560
675
  state: :changed,
561
676
  process: remote_process,
@@ -564,6 +679,22 @@ module GoodData
564
679
  end
565
680
  end
566
681
 
682
+ if has_cycle_trigger
683
+ update_trigger_schedules.each do |update_trigger_schedule|
684
+ working_schedule = update_trigger_schedule[:schedule]
685
+ working_schedule.after = schedule_cache[update_trigger_schedule[:after]]
686
+ working_schedule.trigger_execution_status = schedule_cache[update_trigger_schedule[:trigger_execution_status]] if update_trigger_schedule[:state] == :changed
687
+
688
+ # Update trigger info
689
+ working_schedule.save
690
+
691
+ # Update transfer result
692
+ results.each do |transfer_result|
693
+ transfer_result[:schedule] = working_schedule if transfer_result[:schedule].obj_id == working_schedule.obj_id
694
+ end
695
+ end
696
+ end
697
+
567
698
  diff[:removed].each do |removed_schedule|
568
699
  GoodData.logger.info("Removing schedule #{removed_schedule[:name]}")
569
700
 
@@ -871,6 +1002,14 @@ module GoodData
871
1002
  GoodData::Dashboard[id, project: self, client: client]
872
1003
  end
873
1004
 
1005
+ # Helper for getting analytical dashboards (KD dashboards) of a project
1006
+ #
1007
+ # @param id [String | Number | Object] Anything that you can pass to GoodData::Dashboard[id]
1008
+ # @return [GoodData::AnalyticalDashboard | Array<GoodData::AnalyticalDashboard>] dashboard instance or list
1009
+ def analytical_dashboards(id = :all)
1010
+ GoodData::AnalyticalDashboard[id, project: self, client: client]
1011
+ end
1012
+
874
1013
  def data_permissions(id = :all)
875
1014
  GoodData::MandatoryUserFilter[id, client: client, project: self]
876
1015
  end
@@ -1797,15 +1936,15 @@ module GoodData
1797
1936
  end
1798
1937
 
1799
1938
  # reassign to groups
1939
+ removal_user_group_members = []
1800
1940
  mappings = new_users.map(&:to_hash).flat_map do |user|
1941
+ removal_user_group_members << user[:login] if user[:user_group]&.empty?
1801
1942
  groups = user[:user_group] || []
1802
1943
  groups.map { |g| [user[:login], g] }
1803
1944
  end
1945
+
1804
1946
  unless mappings.empty?
1805
- users_lookup = users.reduce({}) do |a, e|
1806
- a[e.login] = e
1807
- a
1808
- end
1947
+ users_lookup = login_users
1809
1948
  mappings.group_by { |_, g| g }.each do |g, mapping|
1810
1949
  remote_users = mapping.map { |user, _| user }.map { |login| users_lookup[login] && users_lookup[login].uri }.reject(&:nil?)
1811
1950
  GoodData.logger.info("Assigning users #{remote_users} to group #{g}")
@@ -1819,14 +1958,42 @@ module GoodData
1819
1958
  end
1820
1959
  mentioned_groups = mappings.map(&:last).uniq
1821
1960
  groups_to_cleanup = user_groups_cache.reject { |g| mentioned_groups.include?(g.name) }
1961
+
1822
1962
  # clean all groups not mentioned with exception of whitelisted users
1823
1963
  groups_to_cleanup.each do |g|
1824
1964
  g.set_members(whitelist_users(g.members.map(&:to_hash), [], options[:whitelists], :include).first.map { |x| x[:uri] })
1825
1965
  end
1826
1966
  end
1967
+
1968
+ remove_member_from_group(users_lookup, removal_user_group_members, user_groups_cache)
1827
1969
  GoodData::Helpers.join(results, diff_results, [:user], [:login_uri])
1828
1970
  end
1829
1971
 
1972
+ def remove_member_from_group(users_lookup, removal_user_group_members, user_groups_cache)
1973
+ unless removal_user_group_members.empty?
1974
+ users_lookup ||= login_users
1975
+ current_user_groups = user_groups_cache || user_groups
1976
+ removal_user_group_members.uniq.each do |login|
1977
+ user_uri = users_lookup[login]&.uri
1978
+
1979
+ # remove user from group if exists as group member
1980
+ current_user_groups.each do |user_group|
1981
+ if user_group.member?(user_uri)
1982
+ GoodData.logger.info("Removing #{user_uri} user from group #{user_group.name}")
1983
+ user_group.remove_members(user_uri)
1984
+ end
1985
+ end
1986
+ end
1987
+ end
1988
+ end
1989
+
1990
+ def login_users
1991
+ users.reduce({}) do |a, e|
1992
+ a[e.login] = e
1993
+ a
1994
+ end
1995
+ end
1996
+
1830
1997
  def disable_users(list, options = {})
1831
1998
  list = list.map(&:to_hash)
1832
1999
  url = "#{uri}/users"
@@ -2047,6 +2214,14 @@ module GoodData
2047
2214
  GoodData::Project.update_dataset_mapping(model_mapping_json, self)
2048
2215
  end
2049
2216
 
2217
+ def ldm_layout
2218
+ GoodData::Project.get_ldm_layout(self)
2219
+ end
2220
+
2221
+ def save_ldm_layout(ldm_layout_json)
2222
+ GoodData::Project.save_ldm_layout(ldm_layout_json, self)
2223
+ end
2224
+
2050
2225
  def transfer_processes(target)
2051
2226
  GoodData::Project.transfer_processes(self, target)
2052
2227
  end