Skip to content

Commit fecd1fa

Browse files
committed
adjust import across env class and add spec
1 parent 46ae55f commit fecd1fa

File tree

4 files changed

+147
-16
lines changed

4 files changed

+147
-16
lines changed

lib/superset/services/import_dashboard_across_environment.rb

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
=begin
2-
Superset::Services::ImportDashboardAcrossEnvironments
3-
42
This service is used to duplicate a dashboard from one environment to another.
5-
It will not create any database connections from an imported dashboard zip, therefore the target database yml configuration
6-
must already exist in the target superset environment.
3+
It will not create any database connections from an imported dashboard zip, therefore the target_database_yaml_file configuration
4+
must already exist as a database connection in the target superset environment.
75
86
Currently handles only 1 Database yaml file in the zip file. ( ie only 1 common database connection per dashboards datasets )
97
10-
Requirements:
11-
- target_database_yaml_file
12-
- target_database_schema
13-
- dashboard_export_zip
8+
Required Attributes:
9+
- target_database_yaml_file - location of the target database yaml config file
10+
- target_database_schema - the schema name to be used in the target database
11+
- dashboard_export_zip - location of the source dashboard export zip file to tranferred to a new superset Env
1412
1513
Usage:
1614
Assuming you have exported a dashboard from the source environment and have the zip file, and have exported the target database yaml file
15+
1716
Superset::Services::ImportDashboardAcrossEnvironments.new(
1817
target_database_yaml_file: '/tmp/database.yaml',
1918
target_database_schema: 'insert_schema_here',
@@ -22,15 +21,13 @@
2221
2322
=end
2423

25-
#require 'superset/file_utilities'
24+
require 'superset/file_utilities'
2625
require 'yaml'
2726

2827
module Superset
2928
module Services
3029
class ImportDashboardAcrossEnvironments
31-
# include FileUtilities
32-
33-
attr_reader :target_database_yaml_file, :target_database_schema, :dashboard_export_zip
30+
include FileUtilities
3431

3532
def initialize(target_database_yaml_file:, target_database_schema: ,dashboard_export_zip:)
3633
@target_database_yaml_file = target_database_yaml_file
@@ -49,12 +46,14 @@ def perform
4946
create_new_dashboard_zip
5047
end
5148

52-
private
53-
5449
def dashboard_config
5550
@dashboard_config ||= Superset::Services::DashboardLoader.new(dashboard_export_zip: dashboard_export_zip).perform
5651
end
5752

53+
private
54+
55+
attr_reader :target_database_yaml_file, :target_database_schema, :dashboard_export_zip
56+
5857
def remove_source_database_config
5958
return if dashboard_config[:databases].blank?
6059
previous_database_name = dashboard_config[:databases]&.first[:content][:database_name]
@@ -79,13 +78,15 @@ def update_dataset_configs
7978
dashboard_config[:datasets].each do |dataset|
8079
dataset[:content][:database_uuid] = dashboard_config[:databases].first[:content][:uuid]
8180
dataset[:content][:schema] = target_database_schema
82-
File.open(dataset[:filename], 'w') { |f| f.write dataset[:content].to_yaml }
81+
stringified_content = deep_transform_keys_to_strings(dataset[:content])
82+
File.open(dataset[:filename], 'w') { |f| f.write stringified_content.to_yaml }
8383
end
8484
end
8585

8686
def create_new_dashboard_zip
8787
Zip::File.open(new_zip_file, Zip::File::CREATE) do |zipfile|
8888
Dir[File.join(dashboard_export_root_path, '**', '**')].each do |file|
89+
# puts "File Sub:" + file.sub(dashboard_export_root_path + '/', File.basename(dashboard_export_root_path) + '/' ).to_s
8990
zipfile.add(file.sub(dashboard_export_root_path + '/', File.basename(dashboard_export_root_path) + '/' ), file) if File.file?(file)
9091
end
9192
end
@@ -120,10 +121,25 @@ def previous_database_name
120121

121122
def validate_params
122123
raise "Dashboard Export Zip file does not exist" unless File.exist?(dashboard_export_zip)
124+
raise "Dashboard Export Zip file is not a zip file" unless File.extname(dashboard_export_zip) == '.zip'
123125
raise "Target Database YAML file does not exist" unless File.exist?(target_database_yaml_file)
124-
raise "Only 1 Database YAML file is allowed in the zip file" if dashboard_config[:databases].size > 1
126+
raise "Currently this class handles boards with single Database configs only. Multiple Database configs exist in zip file." if dashboard_config[:databases].size > 1
125127
raise "Target Database Schema cannot be blank" if target_database_schema.blank?
126128
end
129+
130+
# Method to recursively transform keys to strings
131+
def deep_transform_keys_to_strings(value)
132+
case value
133+
when Hash
134+
value.each_with_object({}) do |(k, v), result|
135+
result[k.to_s] = deep_transform_keys_to_strings(v)
136+
end
137+
when Array
138+
value.map { |v| deep_transform_keys_to_strings(v) }
139+
else
140+
value
141+
end
142+
end
127143
end
128144
end
129145
end
6.64 KB
Binary file not shown.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
database_name: prod-examples
2+
sqlalchemy_uri: postgresql+psycopg2://superset:XXXXXXXXXX@the-prod-db-host:5432/superset
3+
cache_timeout: null
4+
expose_in_sqllab: true
5+
allow_run_async: true
6+
allow_ctas: true
7+
allow_cvas: true
8+
allow_dml: true
9+
allow_csv_upload: true
10+
extra:
11+
metadata_params: {}
12+
engine_params: {}
13+
metadata_cache_timeout: {}
14+
allows_virtual_table_explore: true
15+
schemas_allowed_for_csv_upload: null
16+
uuid: cc21999b-649f-451f-816a-85b5977b9582
17+
version: 1.0.0
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
require 'superset/services/import_dashboard_across_environment'
2+
3+
RSpec.describe Superset::Services::ImportDashboardAcrossEnvironments do
4+
let(:target_database_yaml_file) { 'spec/fixtures/database-prod-examples.yaml' }
5+
let(:target_database_schema) { 'public' }
6+
let(:dashboard_export_zip) { 'spec/fixtures/dashboard_18_export_20240322.zip' }
7+
let(:service) { described_class.new(
8+
target_database_yaml_file: target_database_yaml_file,
9+
target_database_schema: target_database_schema,
10+
dashboard_export_zip: dashboard_export_zip) }
11+
12+
def list_zip_contents(zip_file_path)
13+
Zip::File.open(zip_file_path) do |zip_file|
14+
zip_file.map(&:name)
15+
end
16+
end
17+
18+
describe '#perform' do
19+
let(:result_zip) { service.perform }
20+
let(:test_unziped_path) { '/tmp/test_import_dashboard_across_env/' }
21+
22+
before do
23+
# unziping the resulting zip to test_unziped_path for inspection
24+
service.unzip_file(result_zip, test_unziped_path)
25+
end
26+
27+
context 'returns a zip file' do
28+
specify 'that is of file type zip' do
29+
expect(File.extname(result_zip)).to eq('.zip')
30+
end
31+
32+
specify 'with the same number of files and directories as the source zip' do
33+
expect(list_zip_contents(result_zip).count).to eq(list_zip_contents(dashboard_export_zip).count)
34+
end
35+
36+
specify 'with exactly 1 database yaml matching the contents of the target_database_yaml included' do
37+
result_zip_database_yamls = Dir.glob(File.join(test_unziped_path, '**', 'databases', '*.yaml'))
38+
expect(result_zip_database_yamls.count).to eq(1)
39+
expect(File.basename(result_zip_database_yamls.first)).to eq(File.basename(target_database_yaml_file))
40+
expect(YAML.load_file(result_zip_database_yamls.first)).to eq(YAML.load_file(target_database_yaml_file))
41+
end
42+
43+
specify 'with dataset yamls updated to include the new target database uuid' do
44+
result_zip_dataset_yamls = Dir.glob(File.join(test_unziped_path, '**', 'datasets', '**', '*.yaml'))
45+
expect(result_zip_dataset_yamls.count).to eq(1)
46+
expect(YAML.load_file(result_zip_dataset_yamls.first)[:database_uuid]).to eq(YAML.load_file(target_database_yaml_file)[:uuid])
47+
end
48+
end
49+
end
50+
51+
describe '#validate_params' do
52+
context 'when all parameters are valid' do
53+
it 'does not raise any errors' do
54+
expect { service.send(:validate_params) }.not_to raise_error
55+
end
56+
end
57+
58+
context 'when dashboard_export_zip does not exist' do
59+
let(:dashboard_export_zip) { 'non-existant-filename.zip' }
60+
61+
it 'raises an error' do
62+
expect { service.send(:validate_params) }.to raise_error(RuntimeError, "Dashboard Export Zip file does not exist")
63+
end
64+
end
65+
66+
context 'when dashboard_export_zip is not a zip file' do
67+
let(:dashboard_export_zip) { 'spec/fixtures/database-prod-examples.yaml' }
68+
69+
it 'raises an error' do
70+
expect { service.send(:validate_params) }.to raise_error(RuntimeError, "Dashboard Export Zip file is not a zip file")
71+
end
72+
end
73+
74+
context 'when target_database_yaml_file does not exist' do
75+
let(:target_database_yaml_file) { 'non-existant-filename.zip' }
76+
77+
it 'raises an error' do
78+
expect { service.send(:validate_params) }.to raise_error(RuntimeError, "Target Database YAML file does not exist")
79+
end
80+
end
81+
82+
context 'when multiple database configs exist in zip file' do
83+
let(:dashboard_export_zip) { 'spec/fixtures/dashboard_export_with_multiple_databases.zip' }
84+
85+
it 'raises an error' do
86+
expect { service.send(:validate_params) }.to raise_error(RuntimeError, "Currently this class handles boards with single Database configs only. Multiple Database configs exist in zip file.")
87+
end
88+
end
89+
90+
context 'when target_database_schema is blank' do
91+
let(:target_database_schema) { '' }
92+
93+
it 'raises an error' do
94+
expect { service.send(:validate_params) }.to raise_error(RuntimeError, "Target Database Schema cannot be blank")
95+
end
96+
end
97+
end
98+
end

0 commit comments

Comments
 (0)