hybrid_platforms_conductor 33.2.0 → 33.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/docs/plugins.md +1 -0
  4. data/docs/plugins/secrets_reader/keepass.md +62 -0
  5. data/lib/hybrid_platforms_conductor/cmd_runner.rb +1 -1
  6. data/lib/hybrid_platforms_conductor/config.rb +0 -35
  7. data/lib/hybrid_platforms_conductor/core_extensions/bundler/without_bundled_env.rb +54 -0
  8. data/lib/hybrid_platforms_conductor/deployer.rb +35 -28
  9. data/lib/hybrid_platforms_conductor/executable.rb +2 -0
  10. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef.rb +4 -3
  11. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox/reserve_proxmox_container +1 -0
  12. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +173 -0
  13. data/lib/hybrid_platforms_conductor/plugins.rb +1 -0
  14. data/lib/hybrid_platforms_conductor/safe_merge.rb +37 -0
  15. data/lib/hybrid_platforms_conductor/topographer/plugins/graphviz.rb +5 -3
  16. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  17. data/spec/hybrid_platforms_conductor_test.rb +4 -0
  18. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/config_dsl_spec.rb +8 -6
  19. data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +15 -1
  20. data/spec/hybrid_platforms_conductor_test/api/config_spec.rb +48 -72
  21. data/spec/hybrid_platforms_conductor_test/api/deployer/config_dsl_spec.rb +36 -0
  22. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/keepass_spec.rb +719 -0
  23. data/spec/hybrid_platforms_conductor_test/api/nodes_handler/cmdbs_plugins_api_spec.rb +2 -2
  24. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/services_deployment_spec.rb +1 -1
  25. data/spec/hybrid_platforms_conductor_test/executables/nodes_to_deploy_spec.rb +21 -15
  26. metadata +159 -139
@@ -1,3 +1,4 @@
1
+ require 'forwardable'
1
2
  require 'hybrid_platforms_conductor/logger_helpers'
2
3
 
3
4
  module HybridPlatformsConductor
@@ -0,0 +1,37 @@
1
+ module HybridPlatformsConductor
2
+
3
+ # Provide an easy way to safe-merge hashes
4
+ module SafeMerge
5
+
6
+ # Safe-merge 2 hashes.
7
+ # Safe-merging is done by:
8
+ # * Merging values that are hashes.
9
+ # * Reporting errors when values conflict.
10
+ # When values are conflicting, the initial hash won't modify those conflicting values and will stop the merge.
11
+ #
12
+ # Parameters::
13
+ # * *hash* (Hash): Hash to be modified merging hash_to_merge
14
+ # * *hash_to_merge* (Hash): Hash to be merged into hash
15
+ # Result::
16
+ # * nil or Array<Object>: nil in case of success, or the keys path leading to a conflicting value in case of error
17
+ def safe_merge(hash, hash_to_merge)
18
+ conflicting_path = nil
19
+ hash_to_merge.each do |key, value_to_merge|
20
+ if hash.key?(key)
21
+ if hash[key].is_a?(Hash) && value_to_merge.is_a?(Hash)
22
+ sub_conflicting_path = safe_merge(hash[key], value_to_merge)
23
+ conflicting_path = [key] + sub_conflicting_path unless sub_conflicting_path.nil?
24
+ elsif hash[key] != value_to_merge
25
+ conflicting_path = [key]
26
+ end
27
+ else
28
+ hash[key] = value_to_merge
29
+ end
30
+ break unless conflicting_path.nil?
31
+ end
32
+ conflicting_path
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -17,9 +17,11 @@ module HybridPlatformsConductor
17
17
  @topographer.force_cluster_strict_hierarchy
18
18
  # Write a Graphviz file
19
19
  File.open(file_name, 'w') do |f|
20
- f.puts 'digraph unix {
21
- size="6,6";
22
- node [style=filled];'
20
+ f.puts <<~EO_GRAPHVIZ
21
+ digraph unix {
22
+ size="6,6";
23
+ node [style=filled];
24
+ EO_GRAPHVIZ
23
25
  # First write the definition of all nodes
24
26
  # Find all nodes belonging to no cluster
25
27
  orphan_nodes = @topographer.nodes_graph.keys
@@ -1,5 +1,5 @@
1
1
  module HybridPlatformsConductor
2
2
 
3
- VERSION = '33.2.0'
3
+ VERSION = '33.3.0'
4
4
 
5
5
  end
@@ -101,6 +101,10 @@ module HybridPlatformsConductorTest
101
101
  ENV.delete 'hpc_user_for_thycotic'
102
102
  ENV.delete 'hpc_password_for_thycotic'
103
103
  ENV.delete 'hpc_domain_for_thycotic'
104
+ ENV.delete 'hpc_user_for_keepass'
105
+ ENV.delete 'hpc_password_for_keepass'
106
+ ENV.delete 'hpc_password_enc_for_keepass'
107
+ ENV.delete 'hpc_key_file_for_keepass'
104
108
  ENV.delete 'hpc_certificates'
105
109
  ENV.delete 'hpc_interactive'
106
110
  ENV.delete 'hpc_test_cookbooks_path'
@@ -13,10 +13,10 @@ describe HybridPlatformsConductor::ActionsExecutor do
13
13
  end
14
14
 
15
15
  it 'returns 1 defined gateway with its content' do
16
- ssh_gateway = '
16
+ ssh_gateway = <<~EO_CONFIG
17
17
  Host gateway
18
18
  Hostname mygateway.com
19
- '
19
+ EO_CONFIG
20
20
  with_repository do
21
21
  with_platforms "gateway :gateway_1, '#{ssh_gateway}'" do
22
22
  expect(test_config.ssh_for_gateway(:gateway_1)).to eq ssh_gateway
@@ -34,10 +34,12 @@ describe HybridPlatformsConductor::ActionsExecutor do
34
34
 
35
35
  it 'returns several defined gateways' do
36
36
  with_repository do
37
- with_platforms '
38
- gateway :gateway_1, \'\'
39
- gateway :gateway_2, \'\'
40
- ' do
37
+ with_platforms(
38
+ <<~EO_CONFIG
39
+ gateway :gateway_1, ''
40
+ gateway :gateway_2, ''
41
+ EO_CONFIG
42
+ ) do
41
43
  expect(test_config.known_gateways.sort).to eq %i[gateway_1 gateway_2].sort
42
44
  end
43
45
  end
@@ -47,7 +47,21 @@ describe HybridPlatformsConductor::CmdRunner do
47
47
 
48
48
  it 'runs a command in an un-bundled environment' do
49
49
  with_repository do
50
- expect(test_cmd_runner.run_cmd('echo "${BUNDLE_GEMFILE}"')).to eq [0, "\n", '']
50
+ %w[
51
+ BUNDLE_GEMFILE
52
+ GEM_HOME
53
+ RUBYOPT
54
+ ].each do |var_to_check|
55
+ expect(test_cmd_runner.run_cmd("echo \"${#{var_to_check}}\"")).to eq [0, "\n", '']
56
+ end
57
+ end
58
+ end
59
+
60
+ it 'keeps dynamically set environment' do
61
+ with_repository do
62
+ value = ('a'..'z').to_a.sample(8).join
63
+ ENV['hpc_test_new_variable'] = value
64
+ expect(test_cmd_runner.run_cmd('echo "${hpc_test_new_variable}"')).to eq [0, "#{value}\n", '']
51
65
  end
52
66
  end
53
67
 
@@ -19,10 +19,12 @@ describe HybridPlatformsConductor::Config do
19
19
  end
20
20
 
21
21
  it 'returns several defined OS images' do
22
- with_platforms '
23
- os_image :image_1, \'/path/to/image_1\'
24
- os_image :image_2, \'/path/to/image_2\'
25
- ' do
22
+ with_platforms(
23
+ <<~EO_CONFIG
24
+ os_image :image_1, '/path/to/image_1'
25
+ os_image :image_2, '/path/to/image_2'
26
+ EO_CONFIG
27
+ ) do
26
28
  expect(test_config.known_os_images.sort).to eq %i[image_1 image_2].sort
27
29
  end
28
30
  end
@@ -49,11 +51,13 @@ describe HybridPlatformsConductor::Config do
49
51
  end
50
52
 
51
53
  it 'includes several configuration files' do
52
- with_platforms '
53
- os_image :image_1, \'/path/to/image_1\'
54
- include_config_from "#{__dir__}/my_conf_1.rb"
55
- include_config_from "#{__dir__}/my_conf_2.rb"
56
- ' do |hybrid_platforms_dir|
54
+ with_platforms(
55
+ <<~'EO_CONFIG'
56
+ os_image :image_1, '/path/to/image_1'
57
+ include_config_from "#{__dir__}/my_conf_1.rb"
58
+ include_config_from "#{__dir__}/my_conf_2.rb"
59
+ EO_CONFIG
60
+ ) do |hybrid_platforms_dir|
57
61
  File.write("#{hybrid_platforms_dir}/my_conf_1.rb", <<~'EO_CONFIG')
58
62
  os_image :image_4, '/path/to/image_4'
59
63
  include_config_from "#{__dir__}/my_conf_3.rb"
@@ -65,9 +69,7 @@ describe HybridPlatformsConductor::Config do
65
69
  end
66
70
 
67
71
  it 'applies nodes specific configuration to all nodes by default' do
68
- with_platforms '
69
- expect_tests_to_fail :my_test, \'Failure reason\'
70
- ' do
72
+ with_platforms 'expect_tests_to_fail :my_test, \'Failure reason\'' do
71
73
  expect(test_config.expected_failures).to eq [
72
74
  {
73
75
  nodes_selectors_stack: [],
@@ -79,12 +81,14 @@ describe HybridPlatformsConductor::Config do
79
81
  end
80
82
 
81
83
  it 'filters nodes specific configuration to nodes sets in a scope' do
82
- with_platforms '
83
- for_nodes(%w[node1 node2 node3]) do
84
- expect_tests_to_fail :my_test_1, \'Failure reason 1\'
85
- end
86
- expect_tests_to_fail :my_test_2, \'Failure reason 2\'
87
- ' do
84
+ with_platforms(
85
+ <<~EO_CONFIG
86
+ for_nodes(%w[node1 node2 node3]) do
87
+ expect_tests_to_fail :my_test_1, 'Failure reason 1'
88
+ end
89
+ expect_tests_to_fail :my_test_2, 'Failure reason 2'
90
+ EO_CONFIG
91
+ ) do
88
92
  sort_proc = proc { |expected_failure_info| expected_failure_info[:reason] }
89
93
  expect(test_config.expected_failures.sort_by(&sort_proc)).to eq [
90
94
  {
@@ -102,14 +106,16 @@ describe HybridPlatformsConductor::Config do
102
106
  end
103
107
 
104
108
  it 'filters nodes specific configuration in a scoped stack' do
105
- with_platforms '
106
- for_nodes(%w[node1 node2 node3]) do
107
- expect_tests_to_fail :my_test_1, \'Failure reason 1\'
108
- for_nodes(%w[node2 node3 node4]) do
109
- expect_tests_to_fail :my_test_2, \'Failure reason 2\'
109
+ with_platforms(
110
+ <<~EO_CONFIG
111
+ for_nodes(%w[node1 node2 node3]) do
112
+ expect_tests_to_fail :my_test_1, 'Failure reason 1'
113
+ for_nodes(%w[node2 node3 node4]) do
114
+ expect_tests_to_fail :my_test_2, 'Failure reason 2'
115
+ end
110
116
  end
111
- end
112
- ' do
117
+ EO_CONFIG
118
+ ) do
113
119
  sort_proc = proc { |expected_failure_info| expected_failure_info[:reason] }
114
120
  expect(test_config.expected_failures.sort_by(&sort_proc)).to eq [
115
121
  {
@@ -127,13 +133,15 @@ describe HybridPlatformsConductor::Config do
127
133
  end
128
134
 
129
135
  it 'returns the expected failures correctly' do
130
- with_platforms '
131
- expect_tests_to_fail :my_test_1, \'Failure reason 1\'
132
- expect_tests_to_fail %i[my_test_2 my_test_3], \'Failure reason 23\'
133
- for_nodes(%w[node1 node2 node3]) do
134
- expect_tests_to_fail :my_test_4, \'Failure reason 4\'
135
- end
136
- ' do
136
+ with_platforms(
137
+ <<~EO_CONFIG
138
+ expect_tests_to_fail :my_test_1, 'Failure reason 1'
139
+ expect_tests_to_fail %i[my_test_2 my_test_3], 'Failure reason 23'
140
+ for_nodes(%w[node1 node2 node3]) do
141
+ expect_tests_to_fail :my_test_4, 'Failure reason 4'
142
+ end
143
+ EO_CONFIG
144
+ ) do
137
145
  sort_proc = proc { |expected_failure_info| expected_failure_info[:reason] }
138
146
  expect(test_config.expected_failures.sort_by(&sort_proc)).to eq [
139
147
  {
@@ -155,47 +163,15 @@ describe HybridPlatformsConductor::Config do
155
163
  end
156
164
  end
157
165
 
158
- it 'returns the retriable errors correctly' do
159
- with_platforms '
160
- retry_deploy_for_errors_on_stdout \'Retry stdout global\'
161
- retry_deploy_for_errors_on_stderr [
162
- \'Retry stderr global\',
163
- /.+Retry stderr regexp global/
164
- ]
165
- for_nodes(%w[node1 node2 node3]) do
166
- retry_deploy_for_errors_on_stdout \'Retry stdout nodes\'
167
- retry_deploy_for_errors_on_stderr \'Retry stderr nodes\'
168
- end
169
- ' do
170
- sort_proc = proc { |retriable_error_info| ((retriable_error_info[:errors_on_stdout] || []) + (retriable_error_info[:errors_on_stderr] || [])).first.to_s }
171
- expect(test_config.retriable_errors.sort_by(&sort_proc)).to eq [
172
- {
173
- nodes_selectors_stack: [],
174
- errors_on_stdout: ['Retry stdout global']
175
- },
176
- {
177
- nodes_selectors_stack: [],
178
- errors_on_stderr: ['Retry stderr global', /.+Retry stderr regexp global/]
179
- },
180
- {
181
- nodes_selectors_stack: [%w[node1 node2 node3]],
182
- errors_on_stdout: ['Retry stdout nodes']
183
- },
184
- {
185
- nodes_selectors_stack: [%w[node1 node2 node3]],
186
- errors_on_stderr: ['Retry stderr nodes']
187
- }
188
- ].sort_by(&sort_proc)
189
- end
190
- end
191
-
192
166
  it 'returns the deployment schedules correctly' do
193
- with_platforms '
194
- deployment_schedule(IceCube::Schedule.new(Time.parse(\'2020-05-01 11:22:33 UTC\')))
195
- for_nodes(%w[node1 node2 node3]) do
196
- deployment_schedule(IceCube::Schedule.new(Time.parse(\'2020-05-02 22:33:44 UTC\')))
197
- end
198
- ' do
167
+ with_platforms(
168
+ <<~EO_CONFIG
169
+ deployment_schedule(IceCube::Schedule.new(Time.parse('2020-05-01 11:22:33 UTC')))
170
+ for_nodes(%w[node1 node2 node3]) do
171
+ deployment_schedule(IceCube::Schedule.new(Time.parse('2020-05-02 22:33:44 UTC')))
172
+ end
173
+ EO_CONFIG
174
+ ) do
199
175
  sort_proc = proc { |deployment_schedule_info| deployment_schedule_info[:schedule].to_ical }
200
176
  expect(test_config.deployment_schedules.sort_by(&sort_proc)).to eq [
201
177
  {
@@ -8,6 +8,42 @@ describe HybridPlatformsConductor::Deployer do
8
8
  end
9
9
  end
10
10
 
11
+ it 'returns the retriable errors correctly' do
12
+ with_platforms(
13
+ <<~EO_CONFIG
14
+ retry_deploy_for_errors_on_stdout 'Retry stdout global'
15
+ retry_deploy_for_errors_on_stderr [
16
+ 'Retry stderr global',
17
+ /.+Retry stderr regexp global/
18
+ ]
19
+ for_nodes(%w[node1 node2 node3]) do
20
+ retry_deploy_for_errors_on_stdout 'Retry stdout nodes'
21
+ retry_deploy_for_errors_on_stderr 'Retry stderr nodes'
22
+ end
23
+ EO_CONFIG
24
+ ) do
25
+ sort_proc = proc { |retriable_error_info| ((retriable_error_info[:errors_on_stdout] || []) + (retriable_error_info[:errors_on_stderr] || [])).first.to_s }
26
+ expect(test_config.retriable_errors.sort_by(&sort_proc)).to eq [
27
+ {
28
+ nodes_selectors_stack: [],
29
+ errors_on_stdout: ['Retry stdout global']
30
+ },
31
+ {
32
+ nodes_selectors_stack: [],
33
+ errors_on_stderr: ['Retry stderr global', /.+Retry stderr regexp global/]
34
+ },
35
+ {
36
+ nodes_selectors_stack: [%w[node1 node2 node3]],
37
+ errors_on_stdout: ['Retry stdout nodes']
38
+ },
39
+ {
40
+ nodes_selectors_stack: [%w[node1 node2 node3]],
41
+ errors_on_stderr: ['Retry stderr nodes']
42
+ }
43
+ ].sort_by(&sort_proc)
44
+ end
45
+ end
46
+
11
47
  it 'declares log plugins to be used' do
12
48
  with_platforms(
13
49
  <<~EO_CONFIG
@@ -0,0 +1,719 @@
1
+ require 'open3'
2
+
3
+ describe HybridPlatformsConductor::Deployer do
4
+
5
+ context 'when checking secrets_reader plugins' do
6
+
7
+ context 'with keepass' do
8
+
9
+ # Expect some calls to be done on KPScript
10
+ #
11
+ # Parameters::
12
+ # * *expected_calls* (Array<[String, String or Hash]>): The list of calls and their corresponding mocked response:
13
+ # * String: Mocked stdout
14
+ # * Hash<Symbol,Object>: More complete structure defining the mocked response:
15
+ # * *exit_status* (Integer): The command exit status [default: 0]
16
+ # * *stdout* (String): The command stdout
17
+ # * *xml* (String or nil): XML document to generate as an export, or nil for none [default: nil]
18
+ def expect_calls_to_kpscript(expected_calls)
19
+ if expected_calls.empty?
20
+ expect(Open3).not_to receive(:popen3)
21
+ else
22
+ expect(Open3).to receive(:popen3).exactly(expected_calls.size).times do |cmd, &block|
23
+ expected_call, mocked_call = expected_calls.shift
24
+ if expected_call.is_a?(Regexp)
25
+ expect(cmd).to match expected_call
26
+ else
27
+ expect(cmd).to eq expected_call
28
+ end
29
+ mocked_call = { stdout: mocked_call } if mocked_call.is_a?(String)
30
+ mocked_call[:exit_status] = 0 unless mocked_call.key?(:exit_status)
31
+ wait_thr_double = instance_double(Process::Waiter)
32
+ allow(wait_thr_double).to receive(:value) do
33
+ wait_thr_value_double = instance_double(Process::Status)
34
+ allow(wait_thr_value_double).to receive(:exitstatus) do
35
+ mocked_call[:exit_status]
36
+ end
37
+ wait_thr_value_double
38
+ end
39
+ if mocked_call[:xml]
40
+ xml_file = cmd.match(/-OutFile:"([^"]+)"/)[1]
41
+ logger.debug "Mock KPScript XML file #{xml_file} with\n#{mocked_call[:xml]}"
42
+ File.write(xml_file, mocked_call[:xml])
43
+ end
44
+ block.call(
45
+ StringIO.new,
46
+ StringIO.new(mocked_call[:stdout]),
47
+ StringIO.new,
48
+ wait_thr_double
49
+ )
50
+ end
51
+ end
52
+ end
53
+
54
+ # Setup a platform for tests
55
+ #
56
+ # Parameters::
57
+ # * *additional_config* (String): Additional config
58
+ # * *platform_info* (Hash): Platform configuration [default: 1 node having 1 service]
59
+ # * *mock_keepass_password* (String): Password to be returned by credentials [default: 'test_keepass_password']
60
+ # * *mock_xml* (String): XML to be mocked [default: xml_single_entry]
61
+ # * *expect_key_file* (String or nil): Key file to be expected, or nil if none [default: nil]
62
+ # * *expect_password_enc* (String or nil): Encrypted password to be expected, or nil if none [default: nil]
63
+ # * *expect_kpscript_calls* (Boolean): Should we expect calls to KPScript? [default: true]
64
+ # * *expect_nbr_credentials_calls* (Integer): How many calls to the credentials are expected? [default: 1]
65
+ # * *block* (Proc): Code called when the platform is setup
66
+ def with_test_platform_for_keepass_test(
67
+ additional_config,
68
+ platform_info: {
69
+ nodes: { 'node' => { services: %w[service] } },
70
+ deployable_services: %w[service]
71
+ },
72
+ mock_keepass_password: 'test_keepass_password',
73
+ mock_xml: xml_single_entry,
74
+ expect_key_file: nil,
75
+ expect_password_enc: nil,
76
+ expect_kpscript_calls: true,
77
+ expect_nbr_credentials_calls: 1,
78
+ &block
79
+ )
80
+ expect(HybridPlatformsConductor::Credentials).to receive(:with_credentials_for).exactly(expect_nbr_credentials_calls).times do |id, _logger, _logger_stderr, url: nil, &client_code|
81
+ expect(id).to eq :keepass
82
+ client_code.call nil, mock_keepass_password
83
+ end
84
+ if expect_kpscript_calls
85
+ expect_calls_to_kpscript [
86
+ [
87
+ %r{/path/to/kpscript "/path/to/database.kdbx"#{mock_keepass_password.nil? ? '' : " -pw:\"#{Regexp.escape(mock_keepass_password)}\""}#{expect_password_enc.nil? ? '' : " -pw-enc:\"#{Regexp.escape(expect_password_enc)}\""}#{expect_key_file.nil? ? '' : " -keyfile:\"#{Regexp.escape(expect_key_file)}\""} -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
88
+ {
89
+ stdout: 'OK: Operation completed successfully.',
90
+ xml: mock_xml
91
+ }
92
+ ]
93
+ ]
94
+ end
95
+ with_test_platform(
96
+ platform_info,
97
+ additional_config: "read_secrets_from :keepass\n#{additional_config}",
98
+ &block
99
+ )
100
+ end
101
+
102
+ # Expect secrets to be set to given values
103
+ #
104
+ # Parameters::
105
+ # * *expected_secrets* (Hash): Expected secrets
106
+ def expect_secrets_to_be(expected_secrets)
107
+ expect(test_services_handler).to receive(:package).with(
108
+ services: { 'node' => %w[service] },
109
+ secrets: expected_secrets,
110
+ local_environment: false
111
+ ) { raise 'Abort as testing secrets is enough' }
112
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Abort as testing secrets is enough'
113
+ end
114
+
115
+ let(:xml_single_entry) do
116
+ <<~EO_XML
117
+ <KeePassFile>
118
+ <Root>
119
+ <Group>
120
+ <Entry>
121
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
122
+ <String>
123
+ <Key>Password</Key>
124
+ <Value ProtectInMemory="True">TestPassword</Value>
125
+ </String>
126
+ <String>
127
+ <Key>Title</Key>
128
+ <Value>Test Secret</Value>
129
+ </String>
130
+ <String>
131
+ <Key>UserName</Key>
132
+ <Value>Test User Name</Value>
133
+ </String>
134
+ </Entry>
135
+ </Group>
136
+ </Root>
137
+ </KeePassFile>
138
+ EO_XML
139
+ end
140
+
141
+ it 'gets secrets from a KeePass database with password' do
142
+ with_test_platform_for_keepass_test(
143
+ <<~EO_CONFIG
144
+ use_kpscript_from '/path/to/kpscript'
145
+ secrets_from_keepass(database: '/path/to/database.kdbx')
146
+ EO_CONFIG
147
+ ) do
148
+ expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' })
149
+ end
150
+ end
151
+
152
+ it 'gets secrets from a KeePass database with password and key file' do
153
+ with_test_platform_for_keepass_test(
154
+ <<~EO_CONFIG,
155
+ use_kpscript_from '/path/to/kpscript'
156
+ secrets_from_keepass(database: '/path/to/database.kdbx')
157
+ EO_CONFIG
158
+ expect_key_file: '/path/to/database.key'
159
+ ) do
160
+ ENV['hpc_key_file_for_keepass'] = '/path/to/database.key'
161
+ expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' })
162
+ end
163
+ end
164
+
165
+ it 'gets secrets from a KeePass database with encrypted password' do
166
+ with_test_platform_for_keepass_test(
167
+ <<~EO_CONFIG,
168
+ use_kpscript_from '/path/to/kpscript'
169
+ secrets_from_keepass(database: '/path/to/database.kdbx')
170
+ EO_CONFIG
171
+ mock_keepass_password: nil,
172
+ expect_password_enc: 'PASSWORD_ENC'
173
+ ) do
174
+ ENV['hpc_password_enc_for_keepass'] = 'PASSWORD_ENC'
175
+ expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' })
176
+ end
177
+ end
178
+
179
+ it 'gets secrets from a KeePass database with encrypted password and key file' do
180
+ with_test_platform_for_keepass_test(
181
+ <<~EO_CONFIG,
182
+ use_kpscript_from '/path/to/kpscript'
183
+ secrets_from_keepass(database: '/path/to/database.kdbx')
184
+ EO_CONFIG
185
+ mock_keepass_password: nil,
186
+ expect_password_enc: 'PASSWORD_ENC',
187
+ expect_key_file: '/path/to/database.key'
188
+ ) do
189
+ ENV['hpc_password_enc_for_keepass'] = 'PASSWORD_ENC'
190
+ ENV['hpc_key_file_for_keepass'] = '/path/to/database.key'
191
+ expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' })
192
+ end
193
+ end
194
+
195
+ it 'gets secrets from a KeePass database with key file' do
196
+ with_test_platform_for_keepass_test(
197
+ <<~EO_CONFIG,
198
+ use_kpscript_from '/path/to/kpscript'
199
+ secrets_from_keepass(database: '/path/to/database.kdbx')
200
+ EO_CONFIG
201
+ mock_keepass_password: nil,
202
+ expect_key_file: '/path/to/database.key'
203
+ ) do
204
+ ENV['hpc_key_file_for_keepass'] = '/path/to/database.key'
205
+ expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' })
206
+ end
207
+ end
208
+
209
+ it 'fails to get secrets from a KeePass database when no authentication mechanisms are provided' do
210
+ with_test_platform_for_keepass_test(
211
+ <<~EO_CONFIG,
212
+ use_kpscript_from '/path/to/kpscript'
213
+ secrets_from_keepass(database: '/path/to/database.kdbx')
214
+ EO_CONFIG
215
+ mock_keepass_password: nil,
216
+ expect_kpscript_calls: false
217
+ ) do
218
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Please specify at least one of password, password_enc or key_file arguments'
219
+ end
220
+ end
221
+
222
+ it 'fails to get secrets if KPScript is not configured' do
223
+ with_test_platform_for_keepass_test(
224
+ <<~EO_CONFIG,
225
+ secrets_from_keepass(database: '/path/to/database.kdbx')
226
+ EO_CONFIG
227
+ expect_nbr_credentials_calls: 0,
228
+ expect_kpscript_calls: false
229
+ ) do
230
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Missing KPScript configuration. Please use use_kpscript_from to set it.'
231
+ end
232
+ end
233
+
234
+ it 'gets secrets from KeePass groups' do
235
+ with_test_platform_for_keepass_test(
236
+ <<~EO_CONFIG,
237
+ use_kpscript_from '/path/to/kpscript'
238
+ secrets_from_keepass(database: '/path/to/database.kdbx')
239
+ EO_CONFIG
240
+ mock_xml: <<~EO_XML
241
+ <KeePassFile>
242
+ <Root>
243
+ <Group>
244
+ <Entry>
245
+ <String>
246
+ <Key>Password</Key>
247
+ <Value ProtectInMemory="True">TestPassword0</Value>
248
+ </String>
249
+ <String>
250
+ <Key>Title</Key>
251
+ <Value>Secret 0</Value>
252
+ </String>
253
+ </Entry>
254
+ <Group>
255
+ <Name>Group1</UUID>
256
+ <Entry>
257
+ <String>
258
+ <Key>Password</Key>
259
+ <Value ProtectInMemory="True">TestPassword1</Value>
260
+ </String>
261
+ <String>
262
+ <Key>Title</Key>
263
+ <Value>Secret 1</Value>
264
+ </String>
265
+ </Entry>
266
+ <Group>
267
+ <Name>Group2</UUID>
268
+ <Entry>
269
+ <String>
270
+ <Key>Password</Key>
271
+ <Value ProtectInMemory="True">TestPassword2</Value>
272
+ </String>
273
+ <String>
274
+ <Key>Title</Key>
275
+ <Value>Secret 2</Value>
276
+ </String>
277
+ </Entry>
278
+ </Group>
279
+ <Group>
280
+ <Name>Group3</UUID>
281
+ <Entry>
282
+ <String>
283
+ <Key>Password</Key>
284
+ <Value ProtectInMemory="True">TestPassword3</Value>
285
+ </String>
286
+ <String>
287
+ <Key>Title</Key>
288
+ <Value>Secret 3</Value>
289
+ </String>
290
+ </Entry>
291
+ </Group>
292
+ </Group>
293
+ </Group>
294
+ </Root>
295
+ </KeePassFile>
296
+ EO_XML
297
+ ) do
298
+ expect_secrets_to_be(
299
+ 'Secret 0' => { 'password' => 'TestPassword0' },
300
+ 'Group1' => {
301
+ 'Secret 1' => { 'password' => 'TestPassword1' },
302
+ 'Group2' => {
303
+ 'Secret 2' => { 'password' => 'TestPassword2' }
304
+ },
305
+ 'Group3' => {
306
+ 'Secret 3' => { 'password' => 'TestPassword3' }
307
+ }
308
+ }
309
+ )
310
+ end
311
+ end
312
+
313
+ it 'gets secrets with attachments' do
314
+ with_test_platform_for_keepass_test(
315
+ <<~EO_CONFIG,
316
+ use_kpscript_from '/path/to/kpscript'
317
+ secrets_from_keepass(database: '/path/to/database.kdbx')
318
+ EO_CONFIG
319
+ mock_xml: <<~EO_XML
320
+ <KeePassFile>
321
+ <Meta>
322
+ <Binaries>
323
+ <Binary ID="0" Compressed="True">#{
324
+ str = StringIO.new
325
+ gz = Zlib::GzipWriter.new(str)
326
+ gz.write('File 0 Content')
327
+ gz.close
328
+ Base64.encode64(str.string).strip
329
+ }</Binary>
330
+ <Binary ID="1">#{Base64.encode64('File 1 Content').strip}</Binary>
331
+ </Binaries>
332
+ </Meta>
333
+ <Root>
334
+ <Group>
335
+ <Entry>
336
+ <String>
337
+ <Key>Password</Key>
338
+ <Value ProtectInMemory="True">TestPassword0</Value>
339
+ </String>
340
+ <String>
341
+ <Key>Title</Key>
342
+ <Value>Secret 0</Value>
343
+ </String>
344
+ <Binary>
345
+ <Key>file0.txt</Key>
346
+ <Value Ref="0" />
347
+ </Binary>
348
+ </Entry>
349
+ <Group>
350
+ <Name>Group1</UUID>
351
+ <Entry>
352
+ <String>
353
+ <Key>Password</Key>
354
+ <Value ProtectInMemory="True">TestPassword1</Value>
355
+ </String>
356
+ <String>
357
+ <Key>Title</Key>
358
+ <Value>Secret 1</Value>
359
+ </String>
360
+ <Binary>
361
+ <Key>file1.txt</Key>
362
+ <Value Ref="1" />
363
+ </Binary>
364
+ </Entry>
365
+ </Group>
366
+ </Group>
367
+ </Root>
368
+ </KeePassFile>
369
+ EO_XML
370
+ ) do
371
+ expect_secrets_to_be(
372
+ 'Secret 0' => { 'file0.txt' => 'File 0 Content', 'password' => 'TestPassword0' },
373
+ 'Group1' => {
374
+ 'Secret 1' => { 'file1.txt' => 'File 1 Content', 'password' => 'TestPassword1' }
375
+ }
376
+ )
377
+ end
378
+ end
379
+
380
+ it 'gets secrets from a KeePass database for several nodes' do
381
+ with_test_platform_for_keepass_test(
382
+ <<~EO_CONFIG,
383
+ use_kpscript_from '/path/to/kpscript'
384
+ secrets_from_keepass(database: '/path/to/database.kdbx')
385
+ EO_CONFIG
386
+ platform_info: {
387
+ nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } },
388
+ deployable_services: %w[service1 service2]
389
+ }
390
+ ) do
391
+ expect(test_services_handler).to receive(:package).with(
392
+ services: { 'node1' => %w[service1], 'node2' => %w[service2] },
393
+ secrets: { 'Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' } },
394
+ local_environment: false
395
+ ) { raise 'Abort as testing secrets is enough' }
396
+ expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Abort as testing secrets is enough'
397
+ end
398
+ end
399
+
400
+ it 'gets secrets from a KeePass database for several databases' do
401
+ with_test_platform_for_keepass_test(
402
+ <<~EO_CONFIG,
403
+ use_kpscript_from '/path/to/kpscript'
404
+ secrets_from_keepass(database: '/path/to/database1.kdbx')
405
+ for_nodes('node2') do
406
+ secrets_from_keepass(database: '/path/to/database2.kdbx')
407
+ end
408
+ EO_CONFIG
409
+ platform_info: {
410
+ nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } },
411
+ deployable_services: %w[service1 service2]
412
+ },
413
+ expect_kpscript_calls: false,
414
+ expect_nbr_credentials_calls: 2
415
+ ) do
416
+ expect_calls_to_kpscript [
417
+ [
418
+ %r{/path/to/kpscript "/path/to/database1.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
419
+ {
420
+ stdout: 'OK: Operation completed successfully.',
421
+ xml: xml_single_entry
422
+ }
423
+ ],
424
+ [
425
+ %r{/path/to/kpscript "/path/to/database2.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
426
+ {
427
+ stdout: 'OK: Operation completed successfully.',
428
+ xml: <<~EO_XML
429
+ <KeePassFile>
430
+ <Root>
431
+ <Group>
432
+ <Entry>
433
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
434
+ <String>
435
+ <Key>Password</Key>
436
+ <Value ProtectInMemory="True">TestPassword2</Value>
437
+ </String>
438
+ <String>
439
+ <Key>Title</Key>
440
+ <Value>Test Secret 2</Value>
441
+ </String>
442
+ <String>
443
+ <Key>UserName</Key>
444
+ <Value>Test User Name 2</Value>
445
+ </String>
446
+ </Entry>
447
+ </Group>
448
+ </Root>
449
+ </KeePassFile>
450
+ EO_XML
451
+ }
452
+ ]
453
+ ]
454
+ expect(test_services_handler).to receive(:package).with(
455
+ services: { 'node1' => %w[service1], 'node2' => %w[service2] },
456
+ secrets: {
457
+ 'Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' },
458
+ 'Test Secret 2' => { 'password' => 'TestPassword2', 'user_name' => 'Test User Name 2' }
459
+ },
460
+ local_environment: false
461
+ ) { raise 'Abort as testing secrets is enough' }
462
+ expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Abort as testing secrets is enough'
463
+ end
464
+ end
465
+
466
+ it 'gets secrets from a group path in a KeePass database' do
467
+ with_test_platform_for_keepass_test(
468
+ <<~EO_CONFIG,
469
+ use_kpscript_from '/path/to/kpscript'
470
+ secrets_from_keepass(
471
+ database: '/path/to/database.kdbx',
472
+ group_path: %w[Group1 Group2 Group3]
473
+ )
474
+ EO_CONFIG
475
+ expect_kpscript_calls: false
476
+ ) do
477
+ expect_calls_to_kpscript [
478
+ [
479
+ %r{/path/to/kpscript "/path/to/database.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+" -GroupPath:"Group1/Group2/Group3"},
480
+ {
481
+ stdout: 'OK: Operation completed successfully.',
482
+ xml: xml_single_entry
483
+ }
484
+ ]
485
+ ]
486
+ expect_secrets_to_be('Test Secret' => { 'password' => 'TestPassword', 'user_name' => 'Test User Name' })
487
+ end
488
+ end
489
+
490
+ it 'merges secrets from several KeePass databases' do
491
+ with_test_platform_for_keepass_test(
492
+ <<~EO_CONFIG,
493
+ use_kpscript_from '/path/to/kpscript'
494
+ secrets_from_keepass(database: '/path/to/database1.kdbx')
495
+ for_nodes('node2') do
496
+ secrets_from_keepass(database: '/path/to/database2.kdbx')
497
+ end
498
+ EO_CONFIG
499
+ platform_info: {
500
+ nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } },
501
+ deployable_services: %w[service1 service2]
502
+ },
503
+ expect_kpscript_calls: false,
504
+ expect_nbr_credentials_calls: 2
505
+ ) do
506
+ expect_calls_to_kpscript [
507
+ [
508
+ %r{/path/to/kpscript "/path/to/database1.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
509
+ {
510
+ stdout: 'OK: Operation completed successfully.',
511
+ xml: <<~EO_XML
512
+ <KeePassFile>
513
+ <Root>
514
+ <Group>
515
+ <Entry>
516
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
517
+ <String>
518
+ <Key>Password</Key>
519
+ <Value ProtectInMemory="True">TestPassword1</Value>
520
+ </String>
521
+ <String>
522
+ <Key>Title</Key>
523
+ <Value>Test Secret 1</Value>
524
+ </String>
525
+ <String>
526
+ <Key>UserName</Key>
527
+ <Value>Test User Name 1</Value>
528
+ </String>
529
+ </Entry>
530
+ <Group>
531
+ <UUID>RsonCc3VHk+k85z5zHhZzQ==</UUID>
532
+ <Name>Group1</Name>
533
+ <Entry>
534
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
535
+ <String>
536
+ <Key>Password</Key>
537
+ <Value ProtectInMemory="True">TestPassword3</Value>
538
+ </String>
539
+ <String>
540
+ <Key>Title</Key>
541
+ <Value>Test Secret 3</Value>
542
+ </String>
543
+ <String>
544
+ <Key>UserName</Key>
545
+ <Value>Test User Name 3</Value>
546
+ </String>
547
+ </Entry>
548
+ </Group>
549
+ </Group>
550
+ </Root>
551
+ </KeePassFile>
552
+ EO_XML
553
+ }
554
+ ],
555
+ [
556
+ %r{/path/to/kpscript "/path/to/database2.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
557
+ {
558
+ stdout: 'OK: Operation completed successfully.',
559
+ xml: <<~EO_XML
560
+ <KeePassFile>
561
+ <Root>
562
+ <Group>
563
+ <Entry>
564
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
565
+ <String>
566
+ <Key>Password</Key>
567
+ <Value ProtectInMemory="True">TestPassword2</Value>
568
+ </String>
569
+ <String>
570
+ <Key>Title</Key>
571
+ <Value>Test Secret 2</Value>
572
+ </String>
573
+ <String>
574
+ <Key>UserName</Key>
575
+ <Value>Test User Name 2</Value>
576
+ </String>
577
+ </Entry>
578
+ <Group>
579
+ <UUID>RsonCc3VHk+k85z5zHhZzQ==</UUID>
580
+ <Name>Group1</Name>
581
+ <Entry>
582
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
583
+ <String>
584
+ <Key>Password</Key>
585
+ <Value ProtectInMemory="True">TestPassword3</Value>
586
+ </String>
587
+ <String>
588
+ <Key>Title</Key>
589
+ <Value>Test Secret 3</Value>
590
+ </String>
591
+ <String>
592
+ <Key>Notes</Key>
593
+ <Value>Notes 3</Value>
594
+ </String>
595
+ </Entry>
596
+ <Entry>
597
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
598
+ <String>
599
+ <Key>Password</Key>
600
+ <Value ProtectInMemory="True">TestPassword4</Value>
601
+ </String>
602
+ <String>
603
+ <Key>Title</Key>
604
+ <Value>Test Secret 4</Value>
605
+ </String>
606
+ <String>
607
+ <Key>UserName</Key>
608
+ <Value>Test User Name 4</Value>
609
+ </String>
610
+ </Entry>
611
+ </Group>
612
+ </Group>
613
+ </Root>
614
+ </KeePassFile>
615
+ EO_XML
616
+ }
617
+ ]
618
+ ]
619
+ expect(test_services_handler).to receive(:package).with(
620
+ services: { 'node1' => %w[service1], 'node2' => %w[service2] },
621
+ secrets: {
622
+ 'Test Secret 1' => { 'password' => 'TestPassword1', 'user_name' => 'Test User Name 1' },
623
+ 'Test Secret 2' => { 'password' => 'TestPassword2', 'user_name' => 'Test User Name 2' },
624
+ 'Group1' => {
625
+ 'Test Secret 3' => { 'password' => 'TestPassword3', 'user_name' => 'Test User Name 3', 'notes' => 'Notes 3' },
626
+ 'Test Secret 4' => { 'password' => 'TestPassword4', 'user_name' => 'Test User Name 4' }
627
+ }
628
+ },
629
+ local_environment: false
630
+ ) { raise 'Abort as testing secrets is enough' }
631
+ expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Abort as testing secrets is enough'
632
+ end
633
+ end
634
+
635
+ it 'fails in case of secrets conflicts between several KeePass databases' do
636
+ with_test_platform_for_keepass_test(
637
+ <<~EO_CONFIG,
638
+ use_kpscript_from '/path/to/kpscript'
639
+ secrets_from_keepass(database: '/path/to/database1.kdbx')
640
+ for_nodes('node2') do
641
+ secrets_from_keepass(database: '/path/to/database2.kdbx')
642
+ end
643
+ EO_CONFIG
644
+ platform_info: {
645
+ nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } },
646
+ deployable_services: %w[service1 service2]
647
+ },
648
+ expect_kpscript_calls: false,
649
+ expect_nbr_credentials_calls: 2
650
+ ) do
651
+ expect_calls_to_kpscript [
652
+ [
653
+ %r{/path/to/kpscript "/path/to/database1.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
654
+ {
655
+ stdout: 'OK: Operation completed successfully.',
656
+ xml: <<~EO_XML
657
+ <KeePassFile>
658
+ <Root>
659
+ <Group>
660
+ <Entry>
661
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
662
+ <String>
663
+ <Key>Password</Key>
664
+ <Value ProtectInMemory="True">TestPassword1</Value>
665
+ </String>
666
+ <String>
667
+ <Key>Title</Key>
668
+ <Value>Test Secret 1</Value>
669
+ </String>
670
+ <String>
671
+ <Key>UserName</Key>
672
+ <Value>Test User Name 1</Value>
673
+ </String>
674
+ </Entry>
675
+ </Group>
676
+ </Root>
677
+ </KeePassFile>
678
+ EO_XML
679
+ }
680
+ ],
681
+ [
682
+ %r{/path/to/kpscript "/path/to/database2.kdbx" -pw:"test_keepass_password" -c:Export -Format:"KeePass XML \(2.x\)" -OutFile:"/tmp/.+"},
683
+ {
684
+ stdout: 'OK: Operation completed successfully.',
685
+ xml: <<~EO_XML
686
+ <KeePassFile>
687
+ <Root>
688
+ <Group>
689
+ <Entry>
690
+ <UUID>Iv3JjMzpPEaijOB+SFZpRw==</UUID>
691
+ <String>
692
+ <Key>Password</Key>
693
+ <Value ProtectInMemory="True">OtherTestPassword1</Value>
694
+ </String>
695
+ <String>
696
+ <Key>Title</Key>
697
+ <Value>Test Secret 1</Value>
698
+ </String>
699
+ <String>
700
+ <Key>UserName</Key>
701
+ <Value>Test User Name 1</Value>
702
+ </String>
703
+ </Entry>
704
+ </Group>
705
+ </Root>
706
+ </KeePassFile>
707
+ EO_XML
708
+ }
709
+ ]
710
+ ]
711
+ expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Secret set at path Test Secret 1->password by /path/to/database2.kdbx for service service2 on node node2 has conflicting values (set debug for value details).'
712
+ end
713
+ end
714
+
715
+ end
716
+
717
+ end
718
+
719
+ end