hybrid_platforms_conductor 33.2.2 → 33.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +29 -2
  4. data/docs/config_dsl.md +45 -0
  5. data/docs/plugins.md +1 -0
  6. data/docs/plugins/secrets_reader/keepass.md +62 -0
  7. data/lib/hybrid_platforms_conductor/bitbucket.rb +134 -90
  8. data/lib/hybrid_platforms_conductor/cmd_runner.rb +4 -4
  9. data/lib/hybrid_platforms_conductor/common_config_dsl/bitbucket.rb +12 -44
  10. data/lib/hybrid_platforms_conductor/common_config_dsl/github.rb +9 -31
  11. data/lib/hybrid_platforms_conductor/config.rb +0 -35
  12. data/lib/hybrid_platforms_conductor/confluence.rb +93 -88
  13. data/lib/hybrid_platforms_conductor/connector.rb +1 -1
  14. data/lib/hybrid_platforms_conductor/core_extensions/bundler/without_bundled_env.rb +18 -1
  15. data/lib/hybrid_platforms_conductor/credentials.rb +122 -97
  16. data/lib/hybrid_platforms_conductor/deployer.rb +37 -30
  17. data/lib/hybrid_platforms_conductor/github.rb +39 -0
  18. data/lib/hybrid_platforms_conductor/hpc_plugins/action/bash.rb +1 -1
  19. data/lib/hybrid_platforms_conductor/hpc_plugins/action/remote_bash.rb +27 -17
  20. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/local.rb +4 -2
  21. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/my_connector.rb.sample +1 -1
  22. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +29 -20
  23. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef.rb +1 -1
  24. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox.rb +5 -3
  25. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox/reserve_proxmox_container +1 -0
  26. data/lib/hybrid_platforms_conductor/hpc_plugins/report/confluence.rb +3 -1
  27. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +174 -0
  28. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/thycotic.rb +3 -1
  29. data/lib/hybrid_platforms_conductor/hpc_plugins/test/bitbucket_conf.rb +4 -1
  30. data/lib/hybrid_platforms_conductor/hpc_plugins/test/github_ci.rb +4 -1
  31. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_conf.rb +7 -3
  32. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_masters_ok.rb +8 -4
  33. data/lib/hybrid_platforms_conductor/hpc_plugins/test_report/confluence.rb +3 -1
  34. data/lib/hybrid_platforms_conductor/logger_helpers.rb +24 -1
  35. data/lib/hybrid_platforms_conductor/plugins.rb +1 -0
  36. data/lib/hybrid_platforms_conductor/safe_merge.rb +37 -0
  37. data/lib/hybrid_platforms_conductor/thycotic.rb +80 -75
  38. data/lib/hybrid_platforms_conductor/topographer/plugins/graphviz.rb +5 -3
  39. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  40. data/spec/hybrid_platforms_conductor_test.rb +10 -0
  41. data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/bash_spec.rb +15 -0
  42. data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/remote_bash_spec.rb +32 -0
  43. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/local/remote_actions_spec.rb +9 -0
  44. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/config_dsl_spec.rb +8 -6
  45. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb +38 -0
  46. data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +21 -1
  47. data/spec/hybrid_platforms_conductor_test/api/config_spec.rb +48 -72
  48. data/spec/hybrid_platforms_conductor_test/api/credentials_spec.rb +251 -0
  49. data/spec/hybrid_platforms_conductor_test/api/deployer/config_dsl_spec.rb +36 -0
  50. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/keepass_spec.rb +680 -0
  51. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/thycotic_spec.rb +2 -2
  52. data/spec/hybrid_platforms_conductor_test/api/nodes_handler/cmdbs_plugins_api_spec.rb +2 -2
  53. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/services_deployment_spec.rb +1 -1
  54. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/bitbucket_conf_spec.rb +49 -69
  55. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/github_ci_spec.rb +29 -39
  56. data/spec/hybrid_platforms_conductor_test/executables/nodes_to_deploy_spec.rb +21 -15
  57. data/spec/hybrid_platforms_conductor_test/test_connector.rb +2 -2
  58. metadata +188 -139
@@ -14,6 +14,8 @@ module HybridPlatformsConductor
14
14
 
15
15
  extend_config_dsl_with CommonConfigDsl::Confluence, :init_confluence
16
16
 
17
+ include HybridPlatformsConductor::Confluence
18
+
17
19
  # Maximum errors to be reported by item
18
20
  MAX_ERROR_ITEMS_DISPLAYED = 10
19
21
 
@@ -28,7 +30,7 @@ module HybridPlatformsConductor
28
30
  confluence_info = @config.confluence_info
29
31
  if confluence_info
30
32
  if confluence_info[:tests_report_page_id]
31
- HybridPlatformsConductor::Confluence.with_confluence(confluence_info[:url], @logger, @logger_stderr) do |confluence|
33
+ with_confluence(confluence_info[:url]) do |confluence|
32
34
  # Get previous percentages for the evolution
33
35
  @previous_success_percentages = confluence.page_storage_format(confluence_info[:tests_report_page_id]).
34
36
  at('h1:contains("Evolution")').
@@ -1,6 +1,23 @@
1
1
  require 'colorize'
2
2
  require 'logger'
3
3
  require 'ruby-progressbar'
4
+ require 'secret_string'
5
+
6
+ # Add colorization methods to SecretString, but always directed to the silenced string as we NEVER want to modiy/clone a secret
7
+ class SecretString
8
+
9
+ extend Colorize::ClassMethods
10
+
11
+ def_delegators :@silenced_str, *%i[
12
+ colorize
13
+ uncolorize
14
+ colorized?
15
+ ]
16
+
17
+ color_methods
18
+ modes_methods
19
+
20
+ end
4
21
 
5
22
  module HybridPlatformsConductor
6
23
 
@@ -88,7 +105,13 @@ module HybridPlatformsConductor
88
105
  define_method("log_#{level}") do |message|
89
106
  (LEVELS_TO_STDERR.include?(level) ? @logger_stderr : @logger).send(
90
107
  level,
91
- defined?(@log_component) ? @log_component : self.class.name.split('::').last
108
+ if defined?(@log_component)
109
+ @log_component
110
+ else
111
+ # Handle the case when the class is unnamed
112
+ class_name = self.class.name
113
+ class_name.nil? ? '<Unnamed class>' : class_name.split('::').last
114
+ end
92
115
  ) { message }
93
116
  end
94
117
  end
@@ -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
@@ -5,95 +5,100 @@ require 'hybrid_platforms_conductor/logger_helpers'
5
5
 
6
6
  module HybridPlatformsConductor
7
7
 
8
- # Gives ways to query the Thycotic SOAP API at a given URL
9
- class Thycotic
8
+ # Mixin giving ways to query the Thycotic SOAP API at a given URL
9
+ module Thycotic
10
10
 
11
- include LoggerHelpers
11
+ include Credentials
12
12
 
13
13
  # Provide a Thycotic connector, and make sure the password is being cleaned when exiting.
14
14
  #
15
15
  # Parameters::
16
16
  # * *thycotic_url* (String): The Thycotic URL
17
- # * *logger* (Logger): Logger to be used
18
- # * *logger_stderr* (Logger): Logger to be used for stderr
19
17
  # * *domain* (String): Domain to use for authentication to Thycotic [default: ENV['hpc_domain_for_thycotic']]
20
18
  # * Proc: Code called with the Thyctotic instance.
21
- # * *thycotic* (Thyctotic): The Thyctotic instance to use.
22
- def self.with_thycotic(thycotic_url, logger, logger_stderr, domain: ENV['hpc_domain_for_thycotic'])
23
- Credentials.with_credentials_for(:thycotic, logger, logger_stderr, url: thycotic_url) do |thycotic_user, thycotic_password|
24
- yield Thycotic.new(thycotic_url, thycotic_user, thycotic_password, domain: domain, logger: logger, logger_stderr: logger_stderr)
19
+ # * *thycotic* (ThyctoticApi): The Thycotic instance to use.
20
+ def with_thycotic(thycotic_url, domain: ENV['hpc_domain_for_thycotic'])
21
+ with_credentials_for(:thycotic, resource: thycotic_url) do |thycotic_user, thycotic_password|
22
+ yield ThycoticApi.new(thycotic_url, thycotic_user, thycotic_password, domain: domain, logger: @logger, logger_stderr: @logger_stderr)
25
23
  end
26
24
  end
27
25
 
28
- # Constructor
29
- #
30
- # Parameters::
31
- # * *url* (String): URL of the Thycotic Secret Server
32
- # * *user* (String): User name to be used to connect to Thycotic
33
- # * *password* (String): Password to be used to connect to Thycotic
34
- # * *domain* (String): Domain to use for authentication to Thycotic [default: ENV['hpc_domain_for_thycotic']]
35
- # * *logger* (Logger): Logger to be used [default: Logger.new(STDOUT)]
36
- # * *logger_stderr* (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]
37
- def initialize(
38
- url,
39
- user,
40
- password,
41
- domain: ENV['hpc_domain_for_thycotic'],
42
- logger: Logger.new($stdout),
43
- logger_stderr: Logger.new($stderr)
44
- )
45
- init_loggers(logger, logger_stderr)
46
- # Get a token to this SOAP API
47
- @client = Savon.client(
48
- wsdl: "#{url}/webservices/SSWebservice.asmx?wsdl",
49
- ssl_verify_mode: :none,
50
- logger: @logger,
51
- log: log_debug?
26
+ # Access to the Thycotic API
27
+ class ThycoticApi
28
+
29
+ include LoggerHelpers
30
+
31
+ # Constructor
32
+ #
33
+ # Parameters::
34
+ # * *url* (String): URL of the Thycotic Secret Server
35
+ # * *user* (String): User name to be used to connect to Thycotic
36
+ # * *password* (SecretString): Password to be used to connect to Thycotic
37
+ # * *domain* (String): Domain to use for authentication to Thycotic [default: ENV['hpc_domain_for_thycotic']]
38
+ # * *logger* (Logger): Logger to be used [default: Logger.new(STDOUT)]
39
+ # * *logger_stderr* (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]
40
+ def initialize(
41
+ url,
42
+ user,
43
+ password,
44
+ domain: ENV['hpc_domain_for_thycotic'],
45
+ logger: Logger.new($stdout),
46
+ logger_stderr: Logger.new($stderr)
52
47
  )
53
- @token = @client.call(
54
- :authenticate,
55
- message: {
56
- username: user,
57
- password: password,
58
- domain: domain
59
- }
60
- ).to_hash.dig(:authenticate_response, :authenticate_result, :token)
61
- raise "Unable to get token from SOAP authentication to #{url}" if @token.nil?
62
- end
48
+ init_loggers(logger, logger_stderr)
49
+ # Get a token to this SOAP API
50
+ @client = Savon.client(
51
+ wsdl: "#{url}/webservices/SSWebservice.asmx?wsdl",
52
+ ssl_verify_mode: :none,
53
+ logger: @logger,
54
+ log: log_debug?
55
+ )
56
+ @token = @client.call(
57
+ :authenticate,
58
+ message: {
59
+ username: user,
60
+ password: password&.to_unprotected,
61
+ domain: domain
62
+ }
63
+ ).to_hash.dig(:authenticate_response, :authenticate_result, :token)
64
+ raise "Unable to get token from SOAP authentication to #{url}" if @token.nil?
65
+ end
63
66
 
64
- # Return secret corresponding to a given secret ID
65
- #
66
- # Parameters::
67
- # * *secret_id* (Object): The secret ID
68
- # Result::
69
- # * Hash: The corresponding API result
70
- def get_secret(secret_id)
71
- @client.call(
72
- :get_secret,
73
- message: {
74
- token: @token,
75
- secretId: secret_id
76
- }
77
- ).to_hash.dig(:get_secret_response, :get_secret_result)
78
- end
67
+ # Return secret corresponding to a given secret ID
68
+ #
69
+ # Parameters::
70
+ # * *secret_id* (Object): The secret ID
71
+ # Result::
72
+ # * Hash: The corresponding API result
73
+ def get_secret(secret_id)
74
+ @client.call(
75
+ :get_secret,
76
+ message: {
77
+ token: @token,
78
+ secretId: secret_id
79
+ }
80
+ ).to_hash.dig(:get_secret_response, :get_secret_result)
81
+ end
82
+
83
+ # Get a file attached to a given secret
84
+ #
85
+ # Parameters::
86
+ # * *secret_id* (Object): The secret ID
87
+ # * *secret_item_id* (Object): The secret item id
88
+ # Result::
89
+ # * String or nil: The file content, or nil if none
90
+ def download_file_attachment_by_item_id(secret_id, secret_item_id)
91
+ encoded_file = @client.call(
92
+ :download_file_attachment_by_item_id,
93
+ message: {
94
+ token: @token,
95
+ secretId: secret_id,
96
+ secretItemId: secret_item_id
97
+ }
98
+ ).to_hash.dig(:download_file_attachment_by_item_id_response, :download_file_attachment_by_item_id_result, :file_attachment)
99
+ encoded_file.nil? ? nil : Base64.decode64(encoded_file)
100
+ end
79
101
 
80
- # Get a file attached to a given secret
81
- #
82
- # Parameters::
83
- # * *secret_id* (Object): The secret ID
84
- # * *secret_item_id* (Object): The secret item id
85
- # Result::
86
- # * String or nil: The file content, or nil if none
87
- def download_file_attachment_by_item_id(secret_id, secret_item_id)
88
- encoded_file = @client.call(
89
- :download_file_attachment_by_item_id,
90
- message: {
91
- token: @token,
92
- secretId: secret_id,
93
- secretItemId: secret_item_id
94
- }
95
- ).to_hash.dig(:download_file_attachment_by_item_id_response, :download_file_attachment_by_item_id_result, :file_attachment)
96
- encoded_file.nil? ? nil : Base64.decode64(encoded_file)
97
102
  end
98
103
 
99
104
  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.2'
3
+ VERSION = '33.5.0'
4
4
 
5
5
  end
@@ -1,5 +1,6 @@
1
1
  require 'fileutils'
2
2
  require 'tmpdir'
3
+ require 'webmock/rspec'
3
4
  require 'hybrid_platforms_conductor/config'
4
5
  require 'hybrid_platforms_conductor/platforms_handler'
5
6
  require 'hybrid_platforms_conductor/actions_executor'
@@ -83,6 +84,9 @@ module HybridPlatformsConductorTest
83
84
  # Make sure the tested components are being reset before each test case
84
85
  RSpec.configure do |config|
85
86
  config.before do
87
+ # We allow for connections by default.
88
+ # Tests that need to test specifically connections at a given point call WebMock.disable_net_connect!
89
+ WebMock.allow_net_connect!
86
90
  @actions_executor = nil
87
91
  @cmd_runner = nil
88
92
  @config = nil
@@ -95,12 +99,18 @@ module HybridPlatformsConductorTest
95
99
  ENV.delete 'hpc_platforms'
96
100
  ENV.delete 'hpc_ssh_gateways_conf'
97
101
  ENV.delete 'hpc_ssh_gateway_user'
102
+ ENV.delete 'hpc_user_for_github'
103
+ ENV.delete 'hpc_password_for_github'
98
104
  ENV.delete 'hpc_user_for_proxmox'
99
105
  ENV.delete 'hpc_password_for_proxmox'
100
106
  ENV.delete 'hpc_realm_for_proxmox'
101
107
  ENV.delete 'hpc_user_for_thycotic'
102
108
  ENV.delete 'hpc_password_for_thycotic'
103
109
  ENV.delete 'hpc_domain_for_thycotic'
110
+ ENV.delete 'hpc_user_for_keepass'
111
+ ENV.delete 'hpc_password_for_keepass'
112
+ ENV.delete 'hpc_password_enc_for_keepass'
113
+ ENV.delete 'hpc_key_file_for_keepass'
104
114
  ENV.delete 'hpc_certificates'
105
115
  ENV.delete 'hpc_interactive'
106
116
  ENV.delete 'hpc_test_cookbooks_path'
@@ -17,6 +17,21 @@ describe HybridPlatformsConductor::ActionsExecutor do
17
17
  end
18
18
  end
19
19
 
20
+ it 'executes local Bash code from a SecretString' do
21
+ with_test_platform_for_action_plugins do |repository|
22
+ expect(
23
+ test_actions_executor.execute_actions(
24
+ {
25
+ 'node' => {
26
+ bash: SecretString.new("echo TestContent >#{repository}/test_file ; echo TestStdout ; echo TestStderr 1>&2", silenced_str: '__INVALID_BASH__')
27
+ }
28
+ }
29
+ )['node']
30
+ ).to eq [0, "TestStdout\n", "TestStderr\n"]
31
+ expect(File.read("#{repository}/test_file")).to eq "TestContent\n"
32
+ end
33
+ end
34
+
20
35
  it 'executes local Bash code with timeout' do
21
36
  with_test_platform_for_action_plugins do
22
37
  expect(test_actions_executor.execute_actions(
@@ -13,6 +13,17 @@ describe HybridPlatformsConductor::ActionsExecutor do
13
13
  end
14
14
  end
15
15
 
16
+ it 'executes remote Bash code from a SecretString' do
17
+ with_test_platform_for_action_plugins do
18
+ test_actions_executor.execute_actions({ 'node' => { remote_bash: SecretString.new('remote_bash_cmd.bash', silenced_str: '__INVALID_BASH__') } })
19
+ expect(test_actions_executor.connector(:test_connector).calls).to eq [
20
+ [:connectable_nodes_from, ['node']],
21
+ [:with_connection_to, ['node'], { no_exception: true }],
22
+ [:remote_bash, 'remote_bash_cmd.bash']
23
+ ]
24
+ end
25
+ end
26
+
16
27
  it 'executes remote Bash code with timeout' do
17
28
  with_test_platform_for_action_plugins do
18
29
  test_actions_executor.connector(:test_connector).remote_bash_code = proc do |_stdout, _stderr, connector|
@@ -124,6 +135,27 @@ describe HybridPlatformsConductor::ActionsExecutor do
124
135
  end
125
136
  end
126
137
 
138
+ it 'executes remote Bash code with environment variables set using SecretStrings' do
139
+ with_test_platform_for_action_plugins do
140
+ test_actions_executor.execute_actions(
141
+ {
142
+ 'node' => { remote_bash: {
143
+ commands: 'bash_cmd.bash',
144
+ env: {
145
+ 'var1' => SecretString.new('value1', silenced_str: 'SILENCED_VALUE'),
146
+ 'var2' => 'value2'
147
+ }
148
+ } }
149
+ }
150
+ )
151
+ expect(test_actions_executor.connector(:test_connector).calls).to eq [
152
+ [:connectable_nodes_from, ['node']],
153
+ [:with_connection_to, ['node'], { no_exception: true }],
154
+ [:remote_bash, "export var1='value1'\nexport var2='value2'\nbash_cmd.bash"]
155
+ ]
156
+ end
157
+ end
158
+
127
159
  end
128
160
 
129
161
  end
@@ -54,6 +54,15 @@ describe HybridPlatformsConductor::ActionsExecutor do
54
54
  end
55
55
  end
56
56
 
57
+ it 'executes bash commands remotely from a SecretString' do
58
+ with_test_platform_for_remote_testing(
59
+ expected_cmds: [['cd /tmp/hpc_local_workspaces/node ; bash_cmd.bash', proc { [0, 'Bash commands executed on node', ''] }]],
60
+ expected_stdout: 'Bash commands executed on node'
61
+ ) do
62
+ test_connector.remote_bash(SecretString.new('bash_cmd.bash', silenced_str: '__INVALID_BASH__'))
63
+ end
64
+ end
65
+
57
66
  it 'executes bash commands remotely with timeout' do
58
67
  with_test_platform_for_remote_testing(
59
68
  expected_cmds: [
@@ -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