hybrid_platforms_conductor 32.18.0 → 33.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +3 -3
  4. data/docs/config_dsl.md +23 -1
  5. data/docs/executables.md +6 -7
  6. data/docs/executables/check-node.md +3 -3
  7. data/docs/executables/deploy.md +3 -3
  8. data/docs/executables/dump_nodes_json.md +3 -3
  9. data/docs/executables/test.md +3 -3
  10. data/docs/executables/topograph.md +3 -3
  11. data/docs/plugins.md +21 -0
  12. data/docs/plugins/secrets_reader/cli.md +31 -0
  13. data/docs/plugins/secrets_reader/thycotic.md +46 -0
  14. data/lib/hybrid_platforms_conductor/deployer.rb +96 -36
  15. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/cli.rb +75 -0
  16. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/my_secrets_reader_plugin.rb.sample +46 -0
  17. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/thycotic.rb +87 -0
  18. data/lib/hybrid_platforms_conductor/plugins.rb +1 -0
  19. data/lib/hybrid_platforms_conductor/secrets_reader.rb +31 -0
  20. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  21. data/spec/hybrid_platforms_conductor_test.rb +5 -0
  22. data/spec/hybrid_platforms_conductor_test/api/deployer/config_dsl_spec.rb +22 -0
  23. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/cli_spec.rb +63 -0
  24. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/thycotic_spec.rb +253 -0
  25. data/spec/hybrid_platforms_conductor_test/executables/options/deployer_spec.rb +0 -182
  26. data/spec/hybrid_platforms_conductor_test/helpers/deployer_test_helpers.rb +249 -13
  27. data/spec/hybrid_platforms_conductor_test/test_secrets_reader_plugin.rb +45 -0
  28. metadata +13 -2
@@ -0,0 +1,75 @@
1
+ require 'hybrid_platforms_conductor/secrets_reader'
2
+
3
+ module HybridPlatformsConductor
4
+
5
+ module HpcPlugins
6
+
7
+ module SecretsReader
8
+
9
+ # Get secrets from the command-line
10
+ class Cli < HybridPlatformsConductor::SecretsReader
11
+
12
+ # Constructor
13
+ #
14
+ # Parameters::
15
+ # * *logger* (Logger): Logger to be used [default: Logger.new(STDOUT)]
16
+ # * *logger_stderr* (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]
17
+ # * *config* (Config): Config to be used. [default: Config.new]
18
+ # * *cmd_runner* (CmdRunner): CmdRunner to be used. [default: CmdRunner.new]
19
+ # * *nodes_handler* (NodesHandler): Nodes handler to be used. [default: NodesHandler.new]
20
+ def initialize(
21
+ logger: Logger.new(STDOUT),
22
+ logger_stderr: Logger.new(STDERR),
23
+ config: Config.new,
24
+ cmd_runner: CmdRunner.new,
25
+ nodes_handler: NodesHandler.new
26
+ )
27
+ super
28
+ @secrets_files = []
29
+ end
30
+
31
+ # Complete an option parser with options meant to control this secrets reader
32
+ # [API] - This method is optional
33
+ #
34
+ # Parameters::
35
+ # * *options_parser* (OptionParser): The option parser to complete
36
+ def options_parse(options_parser)
37
+ options_parser.on('-e', '--secrets JSON_FILE', 'Specify a secrets location from a local JSON file. Can be specified several times.') do |file|
38
+ @secrets_files << file
39
+ end
40
+ end
41
+
42
+ # Return secrets for a given service to be deployed on a node.
43
+ # [API] - This method is mandatory
44
+ # [API] - The following API components are accessible:
45
+ # * *@config* (Config): Main configuration API.
46
+ # * *@cmd_runner* (CmdRunner): Command Runner API.
47
+ # * *@nodes_handler* (NodesHandler): Nodes handler API.
48
+ #
49
+ # Parameters::
50
+ # * *node* (String): Node to be deployed
51
+ # * *service* (String): Service to be deployed
52
+ # Result::
53
+ # * Hash: The secrets
54
+ def secrets_for(node, service)
55
+ # As we are dealing with global secrets, cache the reading for performance between nodes and services.
56
+ unless defined?(@secrets)
57
+ @secrets = {}
58
+ @secrets_files.each do |secrets_file|
59
+ raise "Missing secrets file: #{secrets_file}" unless File.exist?(secrets_file)
60
+ @secrets.merge!(JSON.parse(File.read(secrets_file))) do |key, value1, value2|
61
+ raise "Secret #{key} has conflicting values between different secret JSON files." if value1 != value2
62
+ value1
63
+ end
64
+ end
65
+ end
66
+ @secrets
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ end
@@ -0,0 +1,46 @@
1
+ require 'hybrid_platforms_conductor/secrets_reader'
2
+
3
+ module HybridPlatformsConductor
4
+
5
+ module HpcPlugins
6
+
7
+ module SecretsReader
8
+
9
+ # Read secrets from a secrets source
10
+ class MySecretsReaderPlugin < HybridPlatformsConductor::SecretsReader
11
+
12
+ # Complete an option parser with options meant to control this secrets reader
13
+ # [API] - This method is optional
14
+ #
15
+ # Parameters::
16
+ # * *options_parser* (OptionParser): The option parser to complete
17
+ def options_parse(options_parser)
18
+ @key_file = nil
19
+ options_parser.on('--key-file FILE', 'Key file decrypting a secret vault.') do |file|
20
+ @key_file = file
21
+ end
22
+ end
23
+
24
+ # Return secrets for a given service to be deployed on a node.
25
+ # [API] - This method is mandatory
26
+ # [API] - The following API components are accessible:
27
+ # * *@config* (Config): Main configuration API.
28
+ # * *@cmd_runner* (CmdRunner): Command Runner API.
29
+ # * *@nodes_handler* (NodesHandler): Nodes handler API.
30
+ #
31
+ # Parameters::
32
+ # * *node* (String): Node to be deployed
33
+ # * *service* (String): Service to be deployed
34
+ # Result::
35
+ # * Hash: The secrets
36
+ def secrets_for(node, service)
37
+ JSON.parse(Vault.decrypt("/path/to/#{node}_#{service}.vault", key: @key_file))
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,87 @@
1
+ require 'hybrid_platforms_conductor/secrets_reader'
2
+ require 'hybrid_platforms_conductor/thycotic'
3
+
4
+ module HybridPlatformsConductor
5
+
6
+ module HpcPlugins
7
+
8
+ module SecretsReader
9
+
10
+ # Get secrets from a Thycotic secrets server
11
+ class Thycotic < HybridPlatformsConductor::SecretsReader
12
+
13
+ # Extend the Config DSL
14
+ module ConfigDSLExtension
15
+
16
+ # List of defined Thycotic secrets. Each info has the following properties:
17
+ # * *nodes_selectors_stack* (Array<Object>): Stack of nodes selectors impacted by this rule.
18
+ # * *thycotic_url* (String): Thycotic URL.
19
+ # * *secret_id* (Integer): Thycotic secret ID.
20
+ # Array< Hash<Symbol, Object> >
21
+ attr_reader :thycotic_secrets
22
+
23
+ # Mixin initializer
24
+ def init_thycotic_config
25
+ @thycotic_secrets = []
26
+ end
27
+
28
+ # Set a Thycotic secret server configuration
29
+ #
30
+ # Parameters::
31
+ # * *thycotic_url* (String): The Thycotic server URL.
32
+ # * *secret_id* (Integer): The Thycotic secret ID containing the secrets file to be used as secrets.
33
+ def secrets_from_thycotic(thycotic_url:, secret_id:)
34
+ @thycotic_secrets << {
35
+ nodes_selectors_stack: current_nodes_selectors_stack,
36
+ thycotic_url: thycotic_url,
37
+ secret_id: secret_id
38
+ }
39
+ end
40
+
41
+ end
42
+
43
+ Config.extend_config_dsl_with ConfigDSLExtension, :init_thycotic_config
44
+
45
+ # Return secrets for a given service to be deployed on a node.
46
+ # [API] - This method is mandatory
47
+ # [API] - The following API components are accessible:
48
+ # * *@config* (Config): Main configuration API.
49
+ # * *@cmd_runner* (CmdRunner): Command Runner API.
50
+ # * *@nodes_handler* (NodesHandler): Nodes handler API.
51
+ #
52
+ # Parameters::
53
+ # * *node* (String): Node to be deployed
54
+ # * *service* (String): Service to be deployed
55
+ # Result::
56
+ # * Hash: The secrets
57
+ def secrets_for(node, service)
58
+ secrets = {}
59
+ # As we are dealing with global secrets, cache the reading for performance between nodes and services.
60
+ # Keep secrets cache grouped by URL/ID
61
+ @secrets = {} unless defined?(@secrets)
62
+ @nodes_handler.select_confs_for_node(node, @config.thycotic_secrets).each do |thycotic_secrets_info|
63
+ server_id = "#{thycotic_secrets_info[:thycotic_url]}:#{thycotic_secrets_info[:secret_id]}"
64
+ unless @secrets.key?(server_id)
65
+ HybridPlatformsConductor::Thycotic.with_thycotic(thycotic_secrets_info[:thycotic_url], @logger, @logger_stderr) do |thycotic|
66
+ secret_file_item_id = thycotic.get_secret(thycotic_secrets_info[:secret_id]).dig(:secret, :items, :secret_item, :id)
67
+ raise "Unable to fetch secret file ID #{thycotic_secrets_info[:secret_id]} from #{thycotic_secrets_info[:thycotic_url]}" if secret_file_item_id.nil?
68
+ secret = thycotic.download_file_attachment_by_item_id(thycotic_secrets_info[:secret_id], secret_file_item_id)
69
+ raise "Unable to fetch secret file attachment from secret ID #{thycotic_secrets_info[:secret_id]} from #{thycotic_secrets_info[:thycotic_url]}" if secret.nil?
70
+ @secrets[server_id] = JSON.parse(secret)
71
+ end
72
+ end
73
+ secrets.merge!(@secrets[server_id]) do |key, value1, value2|
74
+ raise "Thycotic secret #{key} served by #{thycotic_secrets_info[:thycotic_url]} from secret ID #{thycotic_secrets_info[:secret_id]} has conflicting values between different secrets." if value1 != value2
75
+ value1
76
+ end
77
+ end
78
+ secrets
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -38,6 +38,7 @@ module HybridPlatformsConductor
38
38
  []
39
39
  each
40
40
  empty?
41
+ find
41
42
  key?
42
43
  keys
43
44
  select
@@ -0,0 +1,31 @@
1
+ require 'hybrid_platforms_conductor/logger_helpers'
2
+ require 'hybrid_platforms_conductor/plugin'
3
+
4
+ module HybridPlatformsConductor
5
+
6
+ # Ancestor of all secrets reader plugins
7
+ class SecretsReader < Plugin
8
+
9
+ # Constructor
10
+ #
11
+ # Parameters::
12
+ # * *logger* (Logger): Logger to be used [default: Logger.new(STDOUT)]
13
+ # * *logger_stderr* (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]
14
+ # * *config* (Config): Config to be used. [default: Config.new]
15
+ # * *cmd_runner* (CmdRunner): CmdRunner to be used. [default: CmdRunner.new]
16
+ # * *nodes_handler* (NodesHandler): Nodes handler to be used. [default: NodesHandler.new]
17
+ def initialize(
18
+ logger: Logger.new(STDOUT),
19
+ logger_stderr: Logger.new(STDERR),
20
+ config: Config.new,
21
+ cmd_runner: CmdRunner.new,
22
+ nodes_handler: NodesHandler.new
23
+ )
24
+ super(logger: logger, logger_stderr: logger_stderr, config: config)
25
+ @cmd_runner = cmd_runner
26
+ @nodes_handler = nodes_handler
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -1,5 +1,5 @@
1
1
  module HybridPlatformsConductor
2
2
 
3
- VERSION = '32.18.0'
3
+ VERSION = '33.0.0'
4
4
 
5
5
  end
@@ -4,6 +4,7 @@ require 'hybrid_platforms_conductor/config'
4
4
  require 'hybrid_platforms_conductor/platforms_handler'
5
5
  require 'hybrid_platforms_conductor/actions_executor'
6
6
  require 'hybrid_platforms_conductor/cmd_runner'
7
+ require 'hybrid_platforms_conductor/credentials'
7
8
  require 'hybrid_platforms_conductor/deployer'
8
9
  require 'hybrid_platforms_conductor/log'
9
10
  require 'hybrid_platforms_conductor/nodes_handler'
@@ -53,6 +54,7 @@ require 'hybrid_platforms_conductor_test/test_plugins/node_ssh'
53
54
  require 'hybrid_platforms_conductor_test/test_plugins/platform'
54
55
  require 'hybrid_platforms_conductor_test/test_plugins/several_checks'
55
56
  require 'hybrid_platforms_conductor_test/test_provisioner'
57
+ require 'hybrid_platforms_conductor_test/test_secrets_reader_plugin'
56
58
  require 'hybrid_platforms_conductor_test/tests_report_plugin'
57
59
 
58
60
  module HybridPlatformsConductorTest
@@ -133,6 +135,9 @@ module HybridPlatformsConductorTest
133
135
  HybridPlatformsConductorTest::TestPlugins::SeveralChecks.runs = []
134
136
  HybridPlatformsConductorTest::TestLogPlugin.calls = []
135
137
  HybridPlatformsConductorTest::TestLogNoReadPlugin.calls = []
138
+ HybridPlatformsConductorTest::TestSecretsReaderPlugin.calls = []
139
+ HybridPlatformsConductorTest::TestSecretsReaderPlugin.deployer = nil
140
+ HybridPlatformsConductorTest::TestSecretsReaderPlugin.mocked_secrets = {}
136
141
  FileUtils.rm_rf './run_logs'
137
142
  FileUtils.rm_rf './testadmin.key.pub'
138
143
  FileUtils.rm_rf '/tmp/hpc_ssh'
@@ -30,6 +30,28 @@ describe HybridPlatformsConductor::Deployer do
30
30
  end
31
31
  end
32
32
 
33
+ it 'declares secrets readers plugins to be used' do
34
+ with_test_platforms(
35
+ { nodes: { 'node1' => {}, 'node2' => {} } },
36
+ false,
37
+ <<~EOS
38
+ read_secrets_from %i[secrets_reader_plugin_1 secrets_reader_plugin_2]
39
+ for_nodes('node2') { read_secrets_from :secrets_reader_plugin_3 }
40
+ EOS
41
+ ) do
42
+ expect(test_config.secrets_readers).to eq [
43
+ {
44
+ nodes_selectors_stack: [],
45
+ secrets_readers: %i[secrets_reader_plugin_1 secrets_reader_plugin_2]
46
+ },
47
+ {
48
+ nodes_selectors_stack: %w[node2],
49
+ secrets_readers: %i[secrets_reader_plugin_3]
50
+ }
51
+ ]
52
+ end
53
+ end
54
+
33
55
  end
34
56
 
35
57
  end
@@ -0,0 +1,63 @@
1
+ describe HybridPlatformsConductor::Deployer do
2
+
3
+ context 'checking secrets_reader plugins' do
4
+
5
+ context 'cli' do
6
+
7
+ # Setup a platform for tests
8
+ #
9
+ # Parameters::
10
+ # * Proc: Code called when the platform is setup
11
+ # * Parameters::
12
+ # * *repository* (String): Platform's repository
13
+ def with_test_platform_for_cli_test
14
+ with_test_platform(
15
+ { nodes: { 'node' => { services: %w[service] } } },
16
+ false,
17
+ 'read_secrets_from :cli'
18
+ ) do |repository|
19
+ yield repository
20
+ end
21
+ end
22
+
23
+ it 'gets secrets from a file' do
24
+ with_test_platform_for_cli_test do |repository|
25
+ secrets_file = "#{repository}/my_secrets.json"
26
+ File.write(secrets_file, '{ "secret_name": "secret_value" }')
27
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
28
+ services: { 'node' => %w[service] },
29
+ secrets: { 'secret_name' => 'secret_value' },
30
+ local_environment: false
31
+ ) { 'Abort as testing secrets is enough' }
32
+ expect { run 'deploy', '--node', 'node', '--secrets', secrets_file }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
33
+ end
34
+ end
35
+
36
+ it 'gets secrets from several files' do
37
+ with_test_platform_for_cli_test do |repository|
38
+ secrets_file1 = "#{repository}/my_secrets1.json"
39
+ File.write(secrets_file1, '{ "secret1": "value1" }')
40
+ secrets_file2 = "#{repository}/my_secrets2.json"
41
+ File.write(secrets_file2, '{ "secret2": "value2" }')
42
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
43
+ services: { 'node' => %w[service] },
44
+ secrets: { 'secret1' => 'value1', 'secret2' => 'value2' },
45
+ local_environment: false
46
+ ) { 'Abort as testing secrets is enough' }
47
+ expect { run 'deploy', '--node', 'node', '--secrets', secrets_file1, '--secrets', secrets_file2 }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
48
+ end
49
+ end
50
+
51
+ it 'fails to get secrets from a missing file' do
52
+ with_test_platform_for_cli_test do
53
+ expect do
54
+ run 'deploy', '--node', 'node', '--secrets', 'unknown_file.json'
55
+ end.to raise_error 'Missing secrets file: unknown_file.json'
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,253 @@
1
+ require 'savon'
2
+
3
+ describe HybridPlatformsConductor::Deployer do
4
+
5
+ context 'checking secrets_reader plugins' do
6
+
7
+ context 'thycotic' do
8
+
9
+ # Setup a platform for tests
10
+ #
11
+ # Parameters::
12
+ # * *additional_config* (String): Additional config
13
+ # * *platform_info* (Hash): Platform configuration [default: 1 node having 1 service]
14
+ # * Proc: Code called when the platform is setup
15
+ def with_test_platform_for_thycotic_test(additional_config = '', platform_info: { nodes: { 'node' => { services: %w[service] } } })
16
+ with_test_platform(
17
+ platform_info,
18
+ false,
19
+ "read_secrets_from :thycotic\n" + additional_config
20
+ ) do
21
+ yield
22
+ end
23
+ end
24
+
25
+ # Mock calls being made to a Thycotic SOAP API using Savon
26
+ #
27
+ # Parameters::
28
+ # * *url* (String): Mocked URL
29
+ # * *secret_id* (String): The mocked secret ID
30
+ # * *mocked_secrets_file* (String or nil): The mocked secrets file stored in Thycotic, or nil to mock a missing secret
31
+ # * *user* (String or nil): The user to be expected, or nil if it should be read from netrc [default: nil]
32
+ # * *password* (String or nil): The password to be expected, or nil if it should be read from netrc [default: nil]
33
+ def mock_thycotic_file_download_on(url, secret_id, mocked_secrets_file, user: nil, password: nil)
34
+ if user.nil?
35
+ user = 'thycotic_user_from_netrc'
36
+ password = 'thycotic_password_from_netrc'
37
+ expect(HybridPlatformsConductor::Credentials).to receive(:with_credentials_for) do |id, _logger, _logger_stderr, url: nil, &client_code|
38
+ expect(id).to eq :thycotic
39
+ expect(url).to eq url
40
+ client_code.call user, password
41
+ end
42
+ end
43
+ # Mock the Savon calls
44
+ mocked_savon_client = double 'Mocked Savon client'
45
+ expect(Savon).to receive(:client) do |params|
46
+ expect(params[:wsdl]).to eq "#{url}/webservices/SSWebservice.asmx?wsdl"
47
+ expect(params[:ssl_verify_mode]).to eq :none
48
+ mocked_savon_client
49
+ end
50
+ expect(mocked_savon_client).to receive(:call).with(
51
+ :authenticate,
52
+ message: {
53
+ username: user,
54
+ password: password,
55
+ domain: 'thycotic_auth_domain'
56
+ }
57
+ ) do
58
+ { authenticate_response: { authenticate_result: { token: 'soap_token' } } }
59
+ end
60
+ expect(mocked_savon_client).to receive(:call).with(
61
+ :get_secret,
62
+ message: {
63
+ token: 'soap_token',
64
+ secretId: secret_id
65
+ }
66
+ ) do
67
+ {
68
+ get_secret_response: {
69
+ get_secret_result:
70
+ if mocked_secrets_file
71
+ { secret: { items: { secret_item: { id: '4242' } } } }
72
+ else
73
+ { errors: { string: 'Access Denied'}, secret_error: { error_code: 'LOAD', error_message: 'Access Denied', allows_response: false } }
74
+ end
75
+ }
76
+ }
77
+ end
78
+ if mocked_secrets_file
79
+ expect(mocked_savon_client).to receive(:call).with(
80
+ :download_file_attachment_by_item_id,
81
+ message: {
82
+ token: 'soap_token',
83
+ secretId: secret_id,
84
+ secretItemId: '4242'
85
+ }
86
+ ) do
87
+ {
88
+ download_file_attachment_by_item_id_response: {
89
+ download_file_attachment_by_item_id_result: {
90
+ file_attachment: Base64.encode64(mocked_secrets_file)
91
+ }
92
+ }
93
+ }
94
+ end
95
+ end
96
+ ENV['hpc_domain_for_thycotic'] = 'thycotic_auth_domain'
97
+ end
98
+
99
+ it 'gets secrets from a Thycotic Secret Server' do
100
+ with_test_platform_for_thycotic_test(
101
+ <<~EOS
102
+ secrets_from_thycotic(
103
+ thycotic_url: 'https://my_thycotic.domain.com/SecretServer',
104
+ secret_id: 1107
105
+ )
106
+ EOS
107
+ ) do
108
+ mock_thycotic_file_download_on('https://my_thycotic.domain.com/SecretServer', 1107, '{ "secret_name": "secret_value" }')
109
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
110
+ services: { 'node' => %w[service] },
111
+ secrets: { 'secret_name' => 'secret_value' },
112
+ local_environment: false
113
+ ) { 'Abort as testing secrets is enough' }
114
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
115
+ end
116
+ end
117
+
118
+ it 'gets secrets from a Thycotic Secret Server for several nodes' do
119
+ additional_config = <<~EOS
120
+ secrets_from_thycotic(
121
+ thycotic_url: 'https://my_thycotic.domain.com/SecretServer',
122
+ secret_id: 1107
123
+ )
124
+ EOS
125
+ with_test_platform_for_thycotic_test(additional_config, platform_info: { nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } } }) do
126
+ mock_thycotic_file_download_on('https://my_thycotic.domain.com/SecretServer', 1107, '{ "secret_name": "secret_value" }')
127
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
128
+ services: { 'node1' => %w[service1], 'node2' => %w[service2] },
129
+ secrets: { 'secret_name' => 'secret_value' },
130
+ local_environment: false
131
+ ) { 'Abort as testing secrets is enough' }
132
+ expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
133
+ end
134
+ end
135
+
136
+ it 'gets secrets from a Thycotic Secret Server using env variables' do
137
+ with_test_platform_for_thycotic_test(
138
+ <<~EOS
139
+ secrets_from_thycotic(
140
+ thycotic_url: 'https://my_thycotic.domain.com/SecretServer',
141
+ secret_id: 1107
142
+ )
143
+ EOS
144
+ ) do
145
+ mock_thycotic_file_download_on(
146
+ 'https://my_thycotic.domain.com/SecretServer',
147
+ 1107,
148
+ '{ "secret_name": "secret_value" }',
149
+ user: 'thycotic_user_from_env',
150
+ password: 'thycotic_password_from_env'
151
+ )
152
+ ENV['hpc_user_for_thycotic'] = 'thycotic_user_from_env'
153
+ ENV['hpc_password_for_thycotic'] = 'thycotic_password_from_env'
154
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
155
+ services: { 'node' => %w[service] },
156
+ secrets: { 'secret_name' => 'secret_value' },
157
+ local_environment: false
158
+ ) { 'Abort as testing secrets is enough' }
159
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
160
+ end
161
+ end
162
+
163
+ it 'gets secrets from several Thycotic Secret Servers' do
164
+ additional_config = <<~EOS
165
+ secrets_from_thycotic(
166
+ thycotic_url: 'https://my_thycotic1.domain.com/SecretServer',
167
+ secret_id: 110701
168
+ )
169
+ for_nodes('node2') do
170
+ secrets_from_thycotic(
171
+ thycotic_url: 'https://my_thycotic2.domain.com/SecretServer',
172
+ secret_id: 110702
173
+ )
174
+ end
175
+ EOS
176
+ with_test_platform_for_thycotic_test(additional_config, platform_info: { nodes: { 'node1' => { services: %w[service1] }, 'node2' => { services: %w[service2] } } }) do
177
+ mock_thycotic_file_download_on('https://my_thycotic1.domain.com/SecretServer', 110701, '{ "secret1": "value1" }')
178
+ mock_thycotic_file_download_on('https://my_thycotic2.domain.com/SecretServer', 110702, '{ "secret2": "value2" }')
179
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
180
+ services: { 'node1' => %w[service1], 'node2' => %w[service2] },
181
+ secrets: { 'secret1' => 'value1', 'secret2' => 'value2' },
182
+ local_environment: false
183
+ ) { 'Abort as testing secrets is enough' }
184
+ expect { test_deployer.deploy_on(%w[node1 node2]) }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
185
+ end
186
+ end
187
+
188
+ it 'merges secrets from several Thycotic Secret Servers' do
189
+ with_test_platform_for_thycotic_test(
190
+ <<~EOS
191
+ secrets_from_thycotic(
192
+ thycotic_url: 'https://my_thycotic1.domain.com/SecretServer',
193
+ secret_id: 110701
194
+ )
195
+ for_nodes('node') do
196
+ secrets_from_thycotic(
197
+ thycotic_url: 'https://my_thycotic2.domain.com/SecretServer',
198
+ secret_id: 110702
199
+ )
200
+ end
201
+ EOS
202
+ ) do
203
+ mock_thycotic_file_download_on('https://my_thycotic1.domain.com/SecretServer', 110701, '{ "secret1": "value1", "secret2": "value2" }')
204
+ mock_thycotic_file_download_on('https://my_thycotic2.domain.com/SecretServer', 110702, '{ "secret2": "value2", "secret3": "value3" }')
205
+ expect(test_services_handler).to receive(:deploy_allowed?).with(
206
+ services: { 'node' => %w[service] },
207
+ secrets: { 'secret1' => 'value1', 'secret2' => 'value2', 'secret3' => 'value3' },
208
+ local_environment: false
209
+ ) { 'Abort as testing secrets is enough' }
210
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Deployment not allowed: Abort as testing secrets is enough'
211
+ end
212
+ end
213
+
214
+ it 'fails in case of secrets conflicts from several Thycotic Secret Servers' do
215
+ with_test_platform_for_thycotic_test(
216
+ <<~EOS
217
+ secrets_from_thycotic(
218
+ thycotic_url: 'https://my_thycotic1.domain.com/SecretServer',
219
+ secret_id: 110701
220
+ )
221
+ for_nodes('node') do
222
+ secrets_from_thycotic(
223
+ thycotic_url: 'https://my_thycotic2.domain.com/SecretServer',
224
+ secret_id: 110702
225
+ )
226
+ end
227
+ EOS
228
+ ) do
229
+ mock_thycotic_file_download_on('https://my_thycotic1.domain.com/SecretServer', 110701, '{ "secret1": "value1", "secret2": "value2" }')
230
+ mock_thycotic_file_download_on('https://my_thycotic2.domain.com/SecretServer', 110702, '{ "secret2": "other_value", "secret3": "value3" }')
231
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Thycotic secret secret2 served by https://my_thycotic2.domain.com/SecretServer from secret ID 110702 has conflicting values between different secrets.'
232
+ end
233
+ end
234
+
235
+ it 'fails to get secrets from a missing Thycotic Secret Server' do
236
+ with_test_platform_for_thycotic_test(
237
+ <<~EOS
238
+ secrets_from_thycotic(
239
+ thycotic_url: 'https://my_thycotic.domain.com/SecretServer',
240
+ secret_id: 1107
241
+ )
242
+ EOS
243
+ ) do
244
+ mock_thycotic_file_download_on('https://my_thycotic.domain.com/SecretServer', 1107, nil)
245
+ expect { test_deployer.deploy_on(%w[node]) }.to raise_error 'Unable to fetch secret file ID 1107 from https://my_thycotic.domain.com/SecretServer'
246
+ end
247
+ end
248
+
249
+ end
250
+
251
+ end
252
+
253
+ end