hybrid_platforms_conductor 33.2.4 → 33.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd578efdc520f51dc9752ace39626e73dbdda736fd4376ee5e9ce55ed34a0a4d
4
- data.tar.gz: c44c935905b7a62a4a0f3ea037b0f1e4ed8fa07bdad4802e38143fe1a448bfd1
3
+ metadata.gz: c0707386d30d5e671d5ceac5e1fec9d971cdccd2b7057c6ed3a75cb5d8bb6d85
4
+ data.tar.gz: e9e5c023662e7aa9283905f4ae54f4b8995d14f61bea1d844ef4fef32cba68dc
5
5
  SHA512:
6
- metadata.gz: f0070cf6736acaa5b9a1e36279bab3b9f6530c26ea341feec4fb4775d022e1c67e2694df832a3ab340b765e10bf510969febe82e4b34fa564e9aab82eea71314
7
- data.tar.gz: d5cc0229b32b7f75d1c7604e304e6986b0b7ab121676dfd62121d954c48e3ba93f827dabfbb8320c9f945339c334263ff9404c8c60e1be5a0b03e4b390cc044f
6
+ metadata.gz: 1b70dedf6605d48081ead37771b8f94e29d26334fb1c0f7f15ed8ed70cabc24809b145bd681af9e829cd0c968accd18cf87886987391709a7252908a00104096
7
+ data.tar.gz: cbe9033432aae4b83b4153501efad2330651696ba0fc8b487d21f5927430d481661e5d3bf2c3617ffb20c2ba7e254f82328d4225aeba4f98295873c55fe4cca9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [v33.3.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v33.2.4...v33.3.0) (2021-07-02 17:20:58)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [[Feature(secrets_reader_keepass)] [#79] Add Keepass secrets reader plugin to get secrets from KeePass databases](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/7ace48653a74f03ed535ee6b41b21242a6454ff3)
7
+
8
+ ## Changes for secrets_reader_keepass
9
+ ### Features
10
+
11
+ * [[Feature(secrets_reader_keepass)] [#79] Add Keepass secrets reader plugin to get secrets from KeePass databases](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/7ace48653a74f03ed535ee6b41b21242a6454ff3)
12
+
1
13
  # [v33.2.4](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v33.2.3...v33.2.4) (2021-06-23 15:14:20)
2
14
 
3
15
  ## Global changes
data/docs/plugins.md CHANGED
@@ -196,6 +196,7 @@ Check the [sample plugin file](../lib/hybrid_platforms_conductor/hpc_plugins/sec
196
196
 
197
197
  Plugins shipped by default:
198
198
  * [`cli`](plugins/secrets_reader/cli.md)
199
+ * [`keepass`](plugins/secrets_reader/keepass.md)
199
200
  * [`thycotic`](plugins/secrets_reader/thycotic.md)
200
201
 
201
202
  <a name="test"></a>
@@ -0,0 +1,62 @@
1
+ # Secrets reader plugin: `keepass`
2
+
3
+ The `keepass` secrets reader plugin retrieves secrets from [KeePass](https://keepass.info/) databases, using an actual KeePass installation with the [KPScript plugin](https://keepass.info/plugins.html#kpscript).
4
+
5
+ It is configured by giving the KPScript command-line (using `use_kpscript_from` config DSL method), the KeePass databases to be read (using `secrets_from_keepass` config DSL method) and uses the `keepass` credential ID to authenticate along with extra environment variables for eventual key files or encrypted passwords.
6
+
7
+ ## Config DSL extension
8
+
9
+ ### `use_kpscript_from`
10
+
11
+ Provide the KPScript command-line to be used. If KPScript is already in your path, using `KPScript.exe` or `kpscript` should be enough, otherwise the full path to the command-line will be needed. On Windows it is needed to also include double quotes if the path contains spaces (like `"C:\Program Files\KeePass\KPScript.exe"`).
12
+
13
+ It takes a simple `String` as parameter to get the command line.
14
+
15
+ Example:
16
+ ```ruby
17
+ use_kpscript_from '/path/to/kpscript'
18
+ ```
19
+
20
+ ### `secrets_from_keepass`
21
+
22
+ Define a KeePass database to read secrets from.
23
+ A base group path of the KeePass database can also be specified to only read secrets from this group path.
24
+
25
+ All entries, attachments and sub-groups from the base group will be read as secrets.
26
+
27
+ Can be applied to subset of nodes using the [`for_nodes` DSL method](/docs/config_dsl.md#for_nodes).
28
+
29
+ It takes the following parameters:
30
+ * **database** (`String`): Database file path.
31
+ * **group_path** (`Array<String>`): Group path to extract from [default: `[]`].
32
+
33
+ Example:
34
+ ```ruby
35
+ secrets_from_keepass(
36
+ database: '/path/to/database.kdbx',
37
+ group_path: %w[Secrets Automation]
38
+ )
39
+ ```
40
+
41
+ ## Used credentials
42
+
43
+ | Credential | Usage
44
+ | --- | --- |
45
+ | `keepass` | Used to get the password to the database. No need to be set if the database opens without password. |
46
+
47
+ ## Used Metadata
48
+
49
+ | Metadata | Type | Usage
50
+ | --- | --- | --- |
51
+
52
+ ## Used environment variables
53
+
54
+ | Variable | Usage
55
+ | --- | --- |
56
+ | `hpc_key_file_for_keepass` | Optional path to the key file needed to open the database |
57
+ | `hpc_password_enc_for_keepass` | Optional encrypted password needed to open the database |
58
+
59
+ ## External tools dependencies
60
+
61
+ * [KeePass](https://keepass.info/) to open databases.
62
+ * [KPScript KeePass plugin](https://keepass.info/plugins.html#kpscript) to query KeePass API.
@@ -9,6 +9,7 @@ require 'hybrid_platforms_conductor/logger_helpers'
9
9
  require 'hybrid_platforms_conductor/nodes_handler'
10
10
  require 'hybrid_platforms_conductor/services_handler'
11
11
  require 'hybrid_platforms_conductor/plugins'
12
+ require 'hybrid_platforms_conductor/safe_merge'
12
13
 
13
14
  module HybridPlatformsConductor
14
15
 
@@ -500,34 +501,7 @@ module HybridPlatformsConductor
500
501
 
501
502
  private
502
503
 
503
- # Safe-merge 2 hashes.
504
- # Safe-merging is done by:
505
- # * Merging values that are hashes.
506
- # * Reporting errors when values conflict.
507
- # When values are conflicting, the initial hash won't modify those conflicting values and will stop the merge.
508
- #
509
- # Parameters::
510
- # * *hash* (Hash): Hash to be modified merging hash_to_merge
511
- # * *hash_to_merge* (Hash): Hash to be merged into hash
512
- # Result::
513
- # * nil or Array<Object>: nil in case of success, or the keys path leading to a conflicting value in case of error
514
- def safe_merge(hash, hash_to_merge)
515
- conflicting_path = nil
516
- hash_to_merge.each do |key, value_to_merge|
517
- if hash.key?(key)
518
- if hash[key].is_a?(Hash) && value_to_merge.is_a?(Hash)
519
- sub_conflicting_path = safe_merge(hash[key], value_to_merge)
520
- conflicting_path = [key] + sub_conflicting_path unless sub_conflicting_path.nil?
521
- elsif hash[key] != value_to_merge
522
- conflicting_path = [key]
523
- end
524
- else
525
- hash[key] = value_to_merge
526
- end
527
- break unless conflicting_path.nil?
528
- end
529
- conflicting_path
530
- end
504
+ include SafeMerge
531
505
 
532
506
  # Get the list of retriable errors a node got from deployment logs.
533
507
  # Useful to know if an error is non-deterministic (due to external and temporary factors).
@@ -0,0 +1,173 @@
1
+ require 'base64'
2
+ require 'nokogiri'
3
+ require 'tempfile'
4
+ require 'keepass_kpscript'
5
+ require 'zlib'
6
+ require 'hybrid_platforms_conductor/credentials'
7
+ require 'hybrid_platforms_conductor/safe_merge'
8
+ require 'hybrid_platforms_conductor/secrets_reader'
9
+
10
+ module HybridPlatformsConductor
11
+
12
+ module HpcPlugins
13
+
14
+ module SecretsReader
15
+
16
+ # Get secrets from a KeePass database
17
+ class Keepass < HybridPlatformsConductor::SecretsReader
18
+
19
+ include SafeMerge
20
+
21
+ # Extend the Config DSL
22
+ module ConfigDSLExtension
23
+
24
+ # List of defined KeePass secrets. Each info has the following properties:
25
+ # * *nodes_selectors_stack* (Array<Object>): Stack of nodes selectors impacted by this rule.
26
+ # * *database* (String): Database file path.
27
+ # * *group_path* (Array<String>): Group path to extract from.
28
+ # Array< Hash<Symbol, Object> >
29
+ attr_reader :keepass_secrets
30
+
31
+ # String: The KPScript command line
32
+ attr_reader :kpscript
33
+
34
+ # Mixin initializer
35
+ def init_keepass_config
36
+ @keepass_secrets = []
37
+ @kpscript = nil
38
+ end
39
+
40
+ # Set the KPScript command line
41
+ #
42
+ # Parameters::
43
+ # * *cmd* (String): KPScript command line
44
+ def use_kpscript_from(cmd)
45
+ @kpscript = cmd
46
+ end
47
+
48
+ # Set a KeePass database configuration
49
+ #
50
+ # Parameters::
51
+ # * *database* (String): Database file path.
52
+ # * *group_path* (Array<String>): Group path to extract from [default: []].
53
+ def secrets_from_keepass(database:, group_path: [])
54
+ @keepass_secrets << {
55
+ nodes_selectors_stack: current_nodes_selectors_stack,
56
+ database: database,
57
+ group_path: group_path
58
+ }
59
+ end
60
+
61
+ end
62
+
63
+ Config.extend_config_dsl_with ConfigDSLExtension, :init_keepass_config
64
+
65
+ # Return secrets for a given service to be deployed on a node.
66
+ # [API] - This method is mandatory
67
+ # [API] - The following API components are accessible:
68
+ # * *@config* (Config): Main configuration API.
69
+ # * *@cmd_runner* (CmdRunner): Command Runner API.
70
+ # * *@nodes_handler* (NodesHandler): Nodes handler API.
71
+ #
72
+ # Parameters::
73
+ # * *node* (String): Node to be deployed
74
+ # * *service* (String): Service to be deployed
75
+ # Result::
76
+ # * Hash: The secrets
77
+ def secrets_for(node, service)
78
+ secrets = {}
79
+ # As we are dealing with global secrets, cache the reading for performance between nodes and services.
80
+ # Keep secrets cache grouped by URL/ID
81
+ @secrets = {} unless defined?(@secrets)
82
+ @nodes_handler.select_confs_for_node(node, @config.keepass_secrets).each do |keepass_secrets_info|
83
+ secret_id = "#{keepass_secrets_info[:database]}:#{keepass_secrets_info[:group_path].join('/')}"
84
+ unless @secrets.key?(secret_id)
85
+ raise 'Missing KPScript configuration. Please use use_kpscript_from to set it.' if @config.kpscript.nil?
86
+
87
+ Credentials.with_credentials_for(:keepass, @logger, @logger_stderr) do |_user, password|
88
+ Tempfile.create('hpc_keepass') do |xml_file|
89
+ key_file = ENV['hpc_key_file_for_keepass']
90
+ password_enc = ENV['hpc_password_enc_for_keepass']
91
+ keepass_credentials = {}
92
+ keepass_credentials[:password] = password if password
93
+ keepass_credentials[:password_enc] = password_enc if password_enc
94
+ keepass_credentials[:key_file] = key_file if key_file
95
+ KeepassKpscript.
96
+ use(@config.kpscript, debug: log_debug?).
97
+ open(keepass_secrets_info[:database], **keepass_credentials).
98
+ export('KeePass XML (2.x)', xml_file.path, group_path: keepass_secrets_info[:group_path].empty? ? nil : keepass_secrets_info[:group_path])
99
+ @secrets[secret_id] = parse_xml_secrets(Nokogiri::XML(xml_file).at_xpath('KeePassFile/Root/Group'))
100
+ end
101
+ end
102
+ end
103
+ conflicting_path = safe_merge(secrets, @secrets[secret_id])
104
+ raise "Secret set at path #{conflicting_path.join('->')} by #{keepass_secrets_info[:database]}#{keepass_secrets_info[:group_path].empty? ? '' : " from group #{keepass_secrets_info[:group_path].join('/')}"} for service #{service} on node #{node} has conflicting values (#{log_debug? ? "#{@secrets[secret_id].dig(*conflicting_path)} != #{secrets.dig(*conflicting_path)}" : 'set debug for value details'})." unless conflicting_path.nil?
105
+ end
106
+ secrets
107
+ end
108
+
109
+ private
110
+
111
+ # List of fields to include in the secrets and their corresponding XML name
112
+ FIELDS = {
113
+ notes: 'Notes',
114
+ password: 'Password',
115
+ url: 'URL',
116
+ user_name: 'UserName'
117
+ }
118
+
119
+ # Parse XML secrets from a Nokogiri XML group node
120
+ #
121
+ # Parameters::
122
+ # * *group* (Nokogiri::XML::Element): The group to parse
123
+ # Result::
124
+ # * Hash: The JSON secrets parsed from this group
125
+ def parse_xml_secrets(group)
126
+ # Parse all entries
127
+ group.xpath('Entry').map do |entry|
128
+ [
129
+ entry.at_xpath('String/Key[contains(.,"Title")]/../Value').text,
130
+ FIELDS.map do |property, field|
131
+ value = entry.at_xpath("String/Key[contains(.,\"#{field}\")]/../Value")&.text
132
+ if value.nil? || value.empty?
133
+ nil
134
+ else
135
+ [
136
+ property.to_s,
137
+ value
138
+ ]
139
+ end
140
+ end.compact.to_h.merge(
141
+ entry.xpath('Binary').map do |binary|
142
+ binary_meta = group.document.at_xpath("KeePassFile/Meta/Binaries/Binary[@ID=#{Integer(binary.xpath('Value').attr('Ref').value)}]")
143
+ binary_content = Base64.decode64(binary_meta.text)
144
+ if binary_meta.attr('Compressed') == 'True'
145
+ gz = Zlib::GzipReader.new(StringIO.new(binary_content))
146
+ binary_content = gz.read
147
+ gz.close
148
+ end
149
+ [
150
+ binary.xpath('Key').text,
151
+ binary_content
152
+ ]
153
+ end.to_h
154
+ )
155
+ ]
156
+ end.to_h.merge(
157
+ # Add children groups
158
+ group.xpath('Group').map do |sub_group|
159
+ [
160
+ sub_group.at_xpath('Name').text,
161
+ parse_xml_secrets(sub_group)
162
+ ]
163
+ end.to_h
164
+ )
165
+ end
166
+
167
+ end
168
+
169
+ end
170
+
171
+ end
172
+
173
+ 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
@@ -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.4'
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
@@ -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
  {
@@ -156,12 +164,14 @@ describe HybridPlatformsConductor::Config do
156
164
  end
157
165
 
158
166
  it 'returns the deployment schedules correctly' do
159
- with_platforms '
160
- deployment_schedule(IceCube::Schedule.new(Time.parse(\'2020-05-01 11:22:33 UTC\')))
161
- for_nodes(%w[node1 node2 node3]) do
162
- deployment_schedule(IceCube::Schedule.new(Time.parse(\'2020-05-02 22:33:44 UTC\')))
163
- end
164
- ' 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
165
175
  sort_proc = proc { |deployment_schedule_info| deployment_schedule_info[:schedule].to_ical }
166
176
  expect(test_config.deployment_schedules.sort_by(&sort_proc)).to eq [
167
177
  {