hybrid_platforms_conductor 32.16.3 → 33.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +6 -3
  4. data/bin/last_deploys +4 -1
  5. data/bin/nodes_to_deploy +5 -5
  6. data/docs/config_dsl.md +45 -1
  7. data/docs/executables.md +6 -7
  8. data/docs/executables/check-node.md +3 -3
  9. data/docs/executables/deploy.md +3 -3
  10. data/docs/executables/dump_nodes_json.md +3 -3
  11. data/docs/executables/test.md +3 -3
  12. data/docs/executables/topograph.md +3 -3
  13. data/docs/gen/mermaid/README.md-0.png +0 -0
  14. data/docs/gen/mermaid/docs/executables/check-node.md-0.png +0 -0
  15. data/docs/gen/mermaid/docs/executables/deploy.md-0.png +0 -0
  16. data/docs/gen/mermaid/docs/executables/free_ips.md-0.png +0 -0
  17. data/docs/gen/mermaid/docs/executables/get_impacted_nodes.md-0.png +0 -0
  18. data/docs/gen/mermaid/docs/executables/last_deploys.md-0.png +0 -0
  19. data/docs/gen/mermaid/docs/executables/nodes_to_deploy.md-0.png +0 -0
  20. data/docs/gen/mermaid/docs/executables/report.md-0.png +0 -0
  21. data/docs/gen/mermaid/docs/executables/run.md-0.png +0 -0
  22. data/docs/gen/mermaid/docs/executables/ssh_config.md-0.png +0 -0
  23. data/docs/gen/mermaid/docs/executables/test.md-0.png +0 -0
  24. data/docs/plugins.md +47 -0
  25. data/docs/plugins/connector/ssh.md +1 -1
  26. data/docs/plugins/log/remote_fs.md +26 -0
  27. data/docs/plugins/secrets_reader/cli.md +31 -0
  28. data/docs/plugins/secrets_reader/thycotic.md +46 -0
  29. data/docs/plugins/test/bitbucket_conf.md +1 -1
  30. data/docs/plugins/test/check_deploy_and_idempotence.md +1 -1
  31. data/docs/plugins/test/connection.md +1 -0
  32. data/docs/plugins/test/deploy_removes_root_access.md +1 -1
  33. data/docs/plugins/test/file_system.md +1 -0
  34. data/docs/plugins/test/github_ci.md +48 -0
  35. data/docs/plugins/test/hostname.md +1 -0
  36. data/docs/plugins/test/ip.md +1 -0
  37. data/docs/plugins/test/jenkins_ci_conf.md +1 -1
  38. data/docs/plugins/test/jenkins_ci_masters_ok.md +1 -1
  39. data/docs/plugins/test/local_users.md +1 -0
  40. data/docs/plugins/test/mounts.md +1 -0
  41. data/docs/plugins/test/orphan_files.md +1 -0
  42. data/docs/plugins/test/ports.md +1 -0
  43. data/docs/plugins/test/spectre.md +1 -0
  44. data/docs/plugins/test/vulnerabilities.md +1 -0
  45. data/lib/hybrid_platforms_conductor/actions_executor.rb +8 -1
  46. data/lib/hybrid_platforms_conductor/common_config_dsl/github.rb +62 -0
  47. data/lib/hybrid_platforms_conductor/deployer.rb +193 -141
  48. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +3 -3
  49. data/lib/hybrid_platforms_conductor/hpc_plugins/log/my_log_plugin.rb.sample +100 -0
  50. data/lib/hybrid_platforms_conductor/hpc_plugins/log/remote_fs.rb +179 -0
  51. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/cli.rb +75 -0
  52. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/my_secrets_reader_plugin.rb.sample +46 -0
  53. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/thycotic.rb +87 -0
  54. data/lib/hybrid_platforms_conductor/hpc_plugins/test/check_deploy_and_idempotence.rb +1 -1
  55. data/lib/hybrid_platforms_conductor/hpc_plugins/test/connection.rb +3 -1
  56. data/lib/hybrid_platforms_conductor/hpc_plugins/test/deploy_freshness.rb +7 -20
  57. data/lib/hybrid_platforms_conductor/hpc_plugins/test/deploy_removes_root_access.rb +1 -1
  58. data/lib/hybrid_platforms_conductor/hpc_plugins/test/file_system.rb +2 -1
  59. data/lib/hybrid_platforms_conductor/hpc_plugins/test/github_ci.rb +32 -0
  60. data/lib/hybrid_platforms_conductor/hpc_plugins/test/hostname.rb +3 -1
  61. data/lib/hybrid_platforms_conductor/hpc_plugins/test/ip.rb +3 -1
  62. data/lib/hybrid_platforms_conductor/hpc_plugins/test/local_users.rb +3 -1
  63. data/lib/hybrid_platforms_conductor/hpc_plugins/test/mounts.rb +3 -1
  64. data/lib/hybrid_platforms_conductor/hpc_plugins/test/orphan_files.rb +3 -1
  65. data/lib/hybrid_platforms_conductor/hpc_plugins/test/ports.rb +3 -1
  66. data/lib/hybrid_platforms_conductor/hpc_plugins/test/spectre.rb +3 -1
  67. data/lib/hybrid_platforms_conductor/hpc_plugins/test/vulnerabilities.rb +2 -1
  68. data/lib/hybrid_platforms_conductor/log.rb +31 -0
  69. data/lib/hybrid_platforms_conductor/plugins.rb +1 -0
  70. data/lib/hybrid_platforms_conductor/secrets_reader.rb +31 -0
  71. data/lib/hybrid_platforms_conductor/test_only_remote_node.rb +18 -0
  72. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  73. data/spec/hybrid_platforms_conductor_test.rb +27 -6
  74. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/connections_spec.rb +3 -3
  75. data/spec/hybrid_platforms_conductor_test/api/deployer/config_dsl_spec.rb +46 -4
  76. data/spec/hybrid_platforms_conductor_test/api/deployer/deploy_spec.rb +187 -212
  77. data/spec/hybrid_platforms_conductor_test/api/deployer/log_plugins/remote_fs_spec.rb +223 -0
  78. data/spec/hybrid_platforms_conductor_test/api/deployer/provisioner_spec.rb +4 -4
  79. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/cli_spec.rb +63 -0
  80. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/thycotic_spec.rb +253 -0
  81. data/spec/hybrid_platforms_conductor_test/api/tests_runner/global_spec.rb +1 -1
  82. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/github_ci_spec.rb +72 -0
  83. data/spec/hybrid_platforms_conductor_test/executables/last_deploys_spec.rb +146 -98
  84. data/spec/hybrid_platforms_conductor_test/executables/nodes_to_deploy_spec.rb +240 -83
  85. data/spec/hybrid_platforms_conductor_test/executables/options/common_spec.rb +2 -1
  86. data/spec/hybrid_platforms_conductor_test/executables/options/deployer_spec.rb +0 -182
  87. data/spec/hybrid_platforms_conductor_test/helpers/connector_ssh_helpers.rb +1 -1
  88. data/spec/hybrid_platforms_conductor_test/helpers/deployer_helpers.rb +40 -53
  89. data/spec/hybrid_platforms_conductor_test/helpers/deployer_test_helpers.rb +251 -15
  90. data/spec/hybrid_platforms_conductor_test/test_log_no_read_plugin.rb +82 -0
  91. data/spec/hybrid_platforms_conductor_test/test_log_plugin.rb +103 -0
  92. data/spec/hybrid_platforms_conductor_test/test_secrets_reader_plugin.rb +45 -0
  93. metadata +41 -2
@@ -0,0 +1,31 @@
1
+ # Secrets reader plugin: `cli`
2
+
3
+ The `cli` secrets reader plugin reads secrets from a local JSON file that can be given through the `--secrets` command-line parameter.
4
+
5
+ Example:
6
+ ```bash
7
+ ./bin/deploy --node my_node --secrets /path/to/my_secrets.json
8
+ ```
9
+
10
+ ## Config DSL extension
11
+
12
+ None
13
+
14
+ ## Used credentials
15
+
16
+ | Credential | Usage
17
+ | --- | --- |
18
+
19
+ ## Used Metadata
20
+
21
+ | Metadata | Type | Usage
22
+ | --- | --- | --- |
23
+
24
+ ## Used environment variables
25
+
26
+ | Variable | Usage
27
+ | --- | --- |
28
+
29
+ ## External tools dependencies
30
+
31
+ None
@@ -0,0 +1,46 @@
1
+ # Secrets reader plugin: `thycotic`
2
+
3
+ The `thycotic` secrets reader plugin retrieves secrets from a [Thycotic secrets server](https://thycotic.com/products/secret-server-vdo/), using its SOAP API.
4
+
5
+ It is configured using the `secrets_from_thycotic` (see below) config DSL and uses the `thycotic` credential ID to authenticate.
6
+
7
+ ## Config DSL extension
8
+
9
+ ### `secrets_from_thycotic`
10
+
11
+ Define a Thycotic URL and Thycotic secret ID to fetch from a Thycotic server.
12
+ The Thycotic secret should contain a JSON file that will be retrieved locally to be used as a secrets source. The local copy will then be removed after deployment.
13
+
14
+ Can be applied to subset of nodes using the [`for_nodes` DSL method](/docs/config_dsl.md#for_nodes).
15
+
16
+ It takes the following parameters:
17
+ * **thycotic_url** (`String`): The Thycotic server URL.
18
+ * **secret_id** (`Integer`): The Thycotic secret ID containing the secrets file to be used as secrets.
19
+
20
+ Example:
21
+ ```ruby
22
+ secrets_from_thycotic(
23
+ thycotic_url: 'https://my-thycotic-server.my-domain.com/SecretServer',
24
+ secret_id: 1107
25
+ )
26
+ ```
27
+
28
+ ## Used credentials
29
+
30
+ | Credential | Usage
31
+ | --- | --- |
32
+ | `thycotic` | Used to authenticate on the Thycotic server's SOAP API |
33
+
34
+ ## Used Metadata
35
+
36
+ | Metadata | Type | Usage
37
+ | --- | --- | --- |
38
+
39
+ ## Used environment variables
40
+
41
+ | Variable | Usage
42
+ | --- | --- |
43
+
44
+ ## External tools dependencies
45
+
46
+ None
@@ -12,7 +12,7 @@ Define a Bitbucket installation to be targeted.
12
12
  It takes the following parameters:
13
13
  * **url** (`String`): URL to the Bitbucket server
14
14
  * **project** (`String`): Project name from the Bitbucket server, storing repositories
15
- * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or :all for all [default: :all]
15
+ * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or `:all` for all [default: `:all`]
16
16
  * **checks** (`Hash<Symbol, Object>`): Checks definition to be perform on those repositories [default: {}]
17
17
  * **branch_permissions** (`Array< Hash<Symbol, Object> >`): List of branch permissions to check [optional]
18
18
  * **type** (`String`): Type of branch permissions to check. Examples of values are 'fast-forward-only', 'no-deletes', 'pull-request-only'.
@@ -49,7 +49,7 @@ end
49
49
 
50
50
  | Metadata | Type | Usage
51
51
  | --- | --- | --- |
52
- | `root_access_allowed` | `String` | If set to `true`, then skip the test for `root` access being disabled after deployment |
52
+ | `root_access_allowed` | `Boolean` | If set to `true`, then skip the test for `root` access being disabled after deployment |
53
53
 
54
54
  ## Used environment variables
55
55
 
@@ -16,6 +16,7 @@ None
16
16
 
17
17
  | Metadata | Type | Usage
18
18
  | --- | --- | --- |
19
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
19
20
 
20
21
  ## Used environment variables
21
22
 
@@ -17,7 +17,7 @@ None
17
17
 
18
18
  | Metadata | Type | Usage
19
19
  | --- | --- | --- |
20
- | `root_access_allowed` | `String` | If set to `true`, then skip this test |
20
+ | `root_access_allowed` | `Boolean` | If set to `true`, then skip this test |
21
21
 
22
22
  ## Used environment variables
23
23
 
@@ -38,6 +38,7 @@ end
38
38
 
39
39
  | Metadata | Type | Usage
40
40
  | --- | --- | --- |
41
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
41
42
 
42
43
  ## Used environment variables
43
44
 
@@ -0,0 +1,48 @@
1
+ # Test plugin: `github_ci`
2
+
3
+ The `github_ci` test plugin checks that the `master` branch of Github repositories has a successful CI result from its [Github Actions](https://github.com/features/actions).
4
+
5
+ ## Config DSL extension
6
+
7
+ ### `github_repos`
8
+
9
+ Define Github repositories to be targeted.
10
+
11
+ It takes the following parameters:
12
+ * **url** (`String`): URL to the Github API [default: `'https://api.github.com'`]
13
+ * **user** (`String`): User or organization name, storing repositories
14
+ * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or `:all` for all [default: `:all`]
15
+
16
+ Example:
17
+ ```ruby
18
+ github_repos(
19
+ # Github's user containing repositories
20
+ user: 'My-Github-User',
21
+ # List of repositories to check
22
+ repos: [
23
+ 'my-platform-repo',
24
+ 'my-chef-repo',
25
+ 'my-hpc-plugins'
26
+ ]
27
+ )
28
+ ```
29
+
30
+ ## Used credentials
31
+
32
+ | Credential | Usage
33
+ | --- | --- |
34
+ | `github` | Used to connect to the Github API. Password should be the Github API token. |
35
+
36
+ ## Used Metadata
37
+
38
+ | Metadata | Type | Usage
39
+ | --- | --- | --- |
40
+
41
+ ## Used environment variables
42
+
43
+ | Variable | Usage
44
+ | --- | --- |
45
+
46
+ ## External tools dependencies
47
+
48
+ None
@@ -16,6 +16,7 @@ None
16
16
 
17
17
  | Metadata | Type | Usage
18
18
  | --- | --- | --- |
19
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
19
20
 
20
21
  ## Used environment variables
21
22
 
@@ -16,6 +16,7 @@ None
16
16
 
17
17
  | Metadata | Type | Usage
18
18
  | --- | --- | --- |
19
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
19
20
  | `private_ips` | `Array<String>` | List of possible private IPs the node should have |
20
21
 
21
22
  ## Used environment variables
@@ -12,7 +12,7 @@ It takes the following parameters:
12
12
  * **url** (`String`): URL to the Bitbucket server
13
13
  * **project** (`String`): Project name from the Bitbucket server, storing repositories
14
14
  * **jenkins_ci_url** (`String` or `nil`): Corresponding Jenkins CI URL, or nil if none.
15
- * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or :all for all [default: :all]
15
+ * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or `:all` for all [default: `:all`]
16
16
 
17
17
  Example:
18
18
  ```ruby
@@ -12,7 +12,7 @@ It takes the following parameters:
12
12
  * **url** (`String`): URL to the Bitbucket server
13
13
  * **project** (`String`): Project name from the Bitbucket server, storing repositories
14
14
  * **jenkins_ci_url** (`String` or `nil`): Corresponding Jenkins CI URL, or nil if none.
15
- * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or :all for all [default: :all]
15
+ * **repos** (`Array<String>` or `Symbol`): List of repository names from this project, or `:all` for all [default: `:all`]
16
16
 
17
17
  Example:
18
18
  ```ruby
@@ -37,6 +37,7 @@ check_local_users_do_not_exist %w[olduser1 olduser2]
37
37
 
38
38
  | Metadata | Type | Usage
39
39
  | --- | --- | --- |
40
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
40
41
 
41
42
  ## Used environment variables
42
43
 
@@ -44,6 +44,7 @@ end
44
44
 
45
45
  | Metadata | Type | Usage
46
46
  | --- | --- | --- |
47
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
47
48
 
48
49
  ## Used environment variables
49
50
 
@@ -27,6 +27,7 @@ end
27
27
 
28
28
  | Metadata | Type | Usage
29
29
  | --- | --- | --- |
30
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
30
31
 
31
32
  ## Used environment variables
32
33
 
@@ -39,6 +39,7 @@ check_closed_ports 25, 110
39
39
  | Metadata | Type | Usage
40
40
  | --- | --- | --- |
41
41
  | `host_ip` | `String` | Host IP address to be tested for port listening |
42
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
42
43
 
43
44
  ## Used environment variables
44
45
 
@@ -15,6 +15,7 @@ None
15
15
 
16
16
  | Metadata | Type | Usage
17
17
  | --- | --- | --- |
18
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
18
19
 
19
20
  ## Used environment variables
20
21
 
@@ -54,6 +54,7 @@ None
54
54
  | Metadata | Type | Usage
55
55
  | --- | --- | --- |
56
56
  | `image` | `String` | The name of the OS image to be used. The [configuration](../../config_dsl.md) should define the image and point it to a directory containing a `oval.json` that will contain definition of OVAL files to be checked for this OS(see above). |
57
+ | `local_node` | `Boolean` | Skip this test for nodes having this metadata set to `true` |
57
58
 
58
59
  ## Used environment variables
59
60
 
@@ -102,7 +102,14 @@ module HybridPlatformsConductor
102
102
  # * *progress_name* (String): Name to display on the progress bar [default: 'Executing actions']
103
103
  # Result::
104
104
  # * Hash<String, [Integer or Symbol, String, String]>: Exit status code (or Symbol in case of error or dry run), standard output and error for each node.
105
- def execute_actions(actions_per_nodes, timeout: nil, concurrent: false, log_to_dir: "#{@config.hybrid_platforms_dir}/run_logs", log_to_stdout: true, progress_name: 'Executing actions')
105
+ def execute_actions(
106
+ actions_per_nodes,
107
+ timeout: nil,
108
+ concurrent: false,
109
+ log_to_dir: "#{@config.hybrid_platforms_dir}/run_logs",
110
+ log_to_stdout: true,
111
+ progress_name: 'Executing actions'
112
+ )
106
113
  # Keep a list of nodes that will need remote access
107
114
  nodes_needing_connectors = []
108
115
  # Compute the ordered list of actions per selected node
@@ -0,0 +1,62 @@
1
+ require 'octokit'
2
+ require 'hybrid_platforms_conductor/credentials'
3
+
4
+ module HybridPlatformsConductor
5
+
6
+ module CommonConfigDsl
7
+
8
+ module Github
9
+
10
+ # Initialize the DSL
11
+ def init_github
12
+ # List of Github repositories definitions
13
+ # Array< Hash<Symbol, Object> >
14
+ # Each definition is just mapping the signature of #github_repos
15
+ @github_repos = []
16
+ end
17
+
18
+ # Register new Github repositories
19
+ #
20
+ # Parameters::
21
+ # * *url* (String): URL to the Github API [default: 'https://api.github.com']
22
+ # * *user* (String): User or organization name, storing repositories
23
+ # * *repos* (Array<String> or Symbol): List of repository names from this project, or :all for all [default: :all]
24
+ def github_repos(url: 'https://api.github.com', user:, repos: :all)
25
+ @github_repos << {
26
+ url: url,
27
+ user: user,
28
+ repos: repos
29
+ }
30
+ end
31
+
32
+ # Iterate over each Github repository
33
+ #
34
+ # Parameters::
35
+ # * Proc: Code called for each Github repository:
36
+ # * Parameters::
37
+ # * *github* (Octokit::Client): The client instance accessing the Github API
38
+ # * *repo_info* (Hash<Symbol, Object>): The repository info:
39
+ # * *name* (String): Repository name.
40
+ # * *slug* (String): Repository slug.
41
+ def for_each_github_repo
42
+ @github_repos.each do |repo_info|
43
+ Octokit.configure do |c|
44
+ c.api_endpoint = repo_info[:url]
45
+ end
46
+ Credentials.with_credentials_for(:github, @logger, @logger_stderr, url: repo_info[:url]) do |_github_user, github_token|
47
+ client = Octokit::Client.new(access_token: github_token)
48
+ (repo_info[:repos] == :all ? client.repositories(repo_info[:user]).map { |repo| repo[:name] } : repo_info[:repos]).each do |name|
49
+ yield client, {
50
+ name: name,
51
+ slug: "#{repo_info[:user]}/#{name}"
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -11,7 +11,6 @@ require 'hybrid_platforms_conductor/logger_helpers'
11
11
  require 'hybrid_platforms_conductor/nodes_handler'
12
12
  require 'hybrid_platforms_conductor/services_handler'
13
13
  require 'hybrid_platforms_conductor/plugins'
14
- require 'hybrid_platforms_conductor/thycotic'
15
14
 
16
15
  module HybridPlatformsConductor
17
16
 
@@ -21,12 +20,26 @@ module HybridPlatformsConductor
21
20
  # Extend the Config DSL
22
21
  module ConfigDSLExtension
23
22
 
23
+ # List of log plugins. Each info has the following properties:
24
+ # * *nodes_selectors_stack* (Array<Object>): Stack of nodes selectors impacted by this rule.
25
+ # * *log_plugins* (Array<Symbol>): List of log plugins to be used to store deployment logs.
26
+ # Array< Hash<Symbol, Object> >
27
+ attr_reader :deployment_logs
28
+
29
+ # List of secrets reader plugins. Each info has the following properties:
30
+ # * *nodes_selectors_stack* (Array<Object>): Stack of nodes selectors impacted by this rule.
31
+ # * *secrets_readers* (Array<Symbol>): List of log plugins to be used to store deployment logs.
32
+ # Array< Hash<Symbol, Object> >
33
+ attr_reader :secrets_readers
34
+
24
35
  # Integer: Timeout (in seconds) for packaging repositories
25
36
  attr_reader :packaging_timeout_secs
26
37
 
27
38
  # Mixin initializer
28
39
  def init_deployer_config
29
40
  @packaging_timeout_secs = 60
41
+ @deployment_logs = []
42
+ @secrets_readers = []
30
43
  end
31
44
 
32
45
  # Set the packaging timeout
@@ -37,6 +50,28 @@ module HybridPlatformsConductor
37
50
  @packaging_timeout_secs = packaging_timeout_secs
38
51
  end
39
52
 
53
+ # Set the deployment log plugins to be used
54
+ #
55
+ # Parameters::
56
+ # * *log_plugins* (Symbol or Array<Symbol>): The list of (or single) log plugins to be used
57
+ def send_logs_to(*log_plugins)
58
+ @deployment_logs << {
59
+ nodes_selectors_stack: current_nodes_selectors_stack,
60
+ log_plugins: log_plugins.flatten
61
+ }
62
+ end
63
+
64
+ # Set the secrets readers
65
+ #
66
+ # Parameters::
67
+ # * *secrets_readers* (Symbol or Array<Symbol>): The list of (or single) secrets readers plugins to be used
68
+ def read_secrets_from(*secrets_readers)
69
+ @secrets_readers << {
70
+ nodes_selectors_stack: current_nodes_selectors_stack,
71
+ secrets_readers: secrets_readers.flatten
72
+ }
73
+ end
74
+
40
75
  end
41
76
 
42
77
  include LoggerHelpers
@@ -55,10 +90,6 @@ module HybridPlatformsConductor
55
90
  # Boolean
56
91
  attr_accessor :concurrent_execution
57
92
 
58
- # The list of JSON secrets
59
- # Array<Hash>
60
- attr_accessor :secrets
61
-
62
93
  # Are we deploying in a local environment?
63
94
  # Boolean
64
95
  attr_accessor :local_environment
@@ -92,8 +123,36 @@ module HybridPlatformsConductor
92
123
  @nodes_handler = nodes_handler
93
124
  @actions_executor = actions_executor
94
125
  @services_handler = services_handler
95
- @secrets = []
126
+ @override_secrets = nil
127
+ @secrets_readers = Plugins.new(
128
+ :secrets_reader,
129
+ logger: @logger,
130
+ logger_stderr: @logger_stderr,
131
+ init_plugin: proc do |plugin_class|
132
+ plugin_class.new(
133
+ logger: @logger,
134
+ logger_stderr: @logger_stderr,
135
+ config: @config,
136
+ cmd_runner: @cmd_runner,
137
+ nodes_handler: @nodes_handler
138
+ )
139
+ end
140
+ )
96
141
  @provisioners = Plugins.new(:provisioner, logger: @logger, logger_stderr: @logger_stderr)
142
+ @log_plugins = Plugins.new(
143
+ :log,
144
+ logger: @logger,
145
+ logger_stderr: @logger_stderr,
146
+ init_plugin: proc do |plugin_class|
147
+ plugin_class.new(
148
+ logger: @logger,
149
+ logger_stderr: @logger_stderr,
150
+ config: @config,
151
+ nodes_handler: @nodes_handler,
152
+ actions_executor: @actions_executor
153
+ )
154
+ end
155
+ )
97
156
  # Default values
98
157
  @use_why_run = false
99
158
  @timeout = nil
@@ -112,30 +171,6 @@ module HybridPlatformsConductor
112
171
  def options_parse(options_parser, parallel_switch: true, why_run_switch: false, timeout_options: true)
113
172
  options_parser.separator ''
114
173
  options_parser.separator 'Deployer options:'
115
- options_parser.on(
116
- '-e', '--secrets SECRETS_LOCATION',
117
- 'Specify a secrets location. Can be specified several times. Location can be:',
118
- '* Local path to a JSON file',
119
- '* URL of the form http[s]://<url>:<secret_id> to get a secret JSON file from a Thycotic Secret Server at the given URL.'
120
- ) do |secrets_location|
121
- @secrets << JSON.parse(
122
- if secrets_location =~ /^(https?:\/\/.+):(\d+)$/
123
- url = $1
124
- secret_id = $2
125
- secret = nil
126
- Thycotic.with_thycotic(url, @logger, @logger_stderr) do |thycotic|
127
- secret_file_item_id = thycotic.get_secret(secret_id).dig(:secret, :items, :secret_item, :id)
128
- raise "Unable to fetch secret file ID #{secrets_location}" if secret_file_item_id.nil?
129
- secret = thycotic.download_file_attachment_by_item_id(secret_id, secret_file_item_id)
130
- raise "Unable to fetch secret file attachment from #{secrets_location}" if secret.nil?
131
- end
132
- secret
133
- else
134
- raise "Missing secret file: #{secrets_location}" unless File.exist?(secrets_location)
135
- File.read(secrets_location)
136
- end
137
- )
138
- end
139
174
  options_parser.on('-p', '--parallel', 'Execute the commands in parallel (put the standard output in files <hybrid-platforms-dir>/run_logs/*.stdout)') do
140
175
  @concurrent_execution = true
141
176
  end if parallel_switch
@@ -148,6 +183,14 @@ module HybridPlatformsConductor
148
183
  options_parser.on('--retries-on-error NBR', "Number of retries in case of non-deterministic errors (defaults to #{@nbr_retries_on_error})") do |nbr_retries|
149
184
  @nbr_retries_on_error = nbr_retries.to_i
150
185
  end
186
+ # Display options secrets readers might have
187
+ @secrets_readers.each do |secret_reader_name, secret_reader|
188
+ if secret_reader.respond_to?(:options_parse)
189
+ options_parser.separator ''
190
+ options_parser.separator "Secrets reader #{secret_reader_name} options:"
191
+ secret_reader.options_parse(options_parser)
192
+ end
193
+ end
151
194
  end
152
195
 
153
196
  # Validate that parsed parameters are valid
@@ -158,6 +201,16 @@ module HybridPlatformsConductor
158
201
  # String: File used as a Futex for packaging
159
202
  PACKAGING_FUTEX_FILE = "#{Dir.tmpdir}/hpc_packaging"
160
203
 
204
+ # Override the secrets with a given JSON.
205
+ # When using this method with a secrets Hash, further deployments will not query secrets readers, but will use those secrets directly.
206
+ # Useful to override secrets in test conditions when using dummy secrets for example.
207
+ #
208
+ # Parameters::
209
+ # * *secrets* (Hash or nil): Secrets to take into account in place of secrets readers, or nil to cancel a previous overriding and use secrets readers instead.
210
+ def override_secrets(secrets)
211
+ @override_secrets = secrets
212
+ end
213
+
161
214
  # Deploy on a given list of nodes selectors.
162
215
  # The workflow is the following:
163
216
  # 1. Package the services to be deployed, considering the nodes, services and context (options, secrets, environment...)
@@ -177,10 +230,20 @@ module HybridPlatformsConductor
177
230
 
178
231
  # Get the secrets to be deployed
179
232
  secrets = {}
180
- @secrets.each do |secret_json|
181
- secrets.merge!(secret_json) do |key, value1, value2|
182
- raise "Secret #{key} has conflicting values between different secret JSON files." if value1 != value2
183
- value1
233
+ if @override_secrets
234
+ secrets = @override_secrets
235
+ else
236
+ services_to_deploy.each do |node, services|
237
+ # If there is no config for secrets, just use cli
238
+ (@config.secrets_readers.empty? ? [{ secrets_readers: %i[cli] }] : @nodes_handler.select_confs_for_node(node, @config.secrets_readers)).inject([]) do |secrets_readers, secrets_readers_info|
239
+ secrets_readers + secrets_readers_info[:secrets_readers]
240
+ end.sort.uniq.each do |secrets_reader|
241
+ services.each do |service|
242
+ node_secrets = @secrets_readers[secrets_reader].secrets_for(node, service)
243
+ conflicting_path = safe_merge(secrets, node_secrets)
244
+ raise "Secret set at path #{conflicting_path.join('->')} by #{secrets_reader} for service #{service} on node #{node} has conflicting values (#{log_debug? ? "#{node_secrets.dig(*conflicting_path)} != #{secrets.dig(*conflicting_path)}" : 'set debug for value details'})." unless conflicting_path.nil?
245
+ end
246
+ end
184
247
  end
185
248
  end
186
249
 
@@ -319,7 +382,7 @@ module HybridPlatformsConductor
319
382
  )
320
383
  instance.with_running_instance(stop_on_exit: true, destroy_on_exit: !reuse_instance, port: 22) do
321
384
  # Test-provisioned nodes have SSH Session Exec capabilities and are not local
322
- sub_executable.nodes_handler.override_metadata_of node, :ssh_session_exec, 'true'
385
+ sub_executable.nodes_handler.override_metadata_of node, :ssh_session_exec, true
323
386
  sub_executable.nodes_handler.override_metadata_of node, :local_node, false
324
387
  # Test-provisioned nodes use default sudo
325
388
  sub_executable.config.sudo_procs.replace(sub_executable.config.sudo_procs.map do |sudo_proc_info|
@@ -338,7 +401,7 @@ module HybridPlatformsConductor
338
401
  deployer.local_environment = true
339
402
  # Ignore secrets that might have been given: in Docker containers we always use dummy secrets
340
403
  dummy_secrets_file = "#{@config.hybrid_platforms_dir}/dummy_secrets.json"
341
- deployer.secrets = File.exist?(dummy_secrets_file) ? [JSON.parse(File.read(dummy_secrets_file))] : []
404
+ deployer.override_secrets(File.exist?(dummy_secrets_file) ? JSON.parse(File.read(dummy_secrets_file)) : {})
342
405
  yield deployer, instance
343
406
  end
344
407
  rescue
@@ -355,72 +418,31 @@ module HybridPlatformsConductor
355
418
  # * *nodes* (Array<String>): Nodes to get info from
356
419
  # Result::
357
420
  # * Hash<String, Hash<Symbol,Object>: The deployed info, per node name.
358
- # Properties are defined by the Deployer#save_logs method, and additionally to them the following properties can be set:
359
- # * *error* (String): Optional property set in case of error
421
+ # * *error* (String): Error string in case deployment logs could not be retrieved. If set then further properties will be ignored. [optional]
422
+ # * *services* (Array<String>): List of services deployed on the node
423
+ # * *deployment_info* (Hash<Symbol,Object>): Deployment metadata
424
+ # * *exit_status* (Integer or Symbol): Deployment exit status
425
+ # * *stdout* (String): Deployment stdout
426
+ # * *stderr* (String): Deployment stderr
360
427
  def deployment_info_from(*nodes)
428
+ nodes = nodes.flatten
361
429
  @actions_executor.max_threads = 64
362
- Hash[@actions_executor.
363
- execute_actions(
364
- Hash[nodes.flatten.map do |node|
365
- [
366
- node,
367
- { remote_bash: "cd /var/log/deployments && ls -t | head -1 | xargs sed '/===== STDOUT =====/q'" }
368
- ]
369
- end],
370
- log_to_stdout: false,
371
- concurrent: true,
372
- timeout: 10,
373
- progress_name: 'Getting deployment info'
374
- ).
375
- map do |node, (exit_status, stdout, stderr)|
376
- # Expected format for stdout:
377
- # Property1: Value1
378
- # ...
379
- # PropertyN: ValueN
380
- # ===== STDOUT =====
381
- # ...
382
- deploy_info = {}
383
- if exit_status.is_a?(Symbol)
384
- deploy_info[:error] = "Error: #{exit_status}\n#{stderr}"
385
- else
386
- stdout_lines = stdout.split("\n")
387
- if stdout_lines.first =~ /No such file or directory/
388
- deploy_info[:error] = '/var/log/deployments missing'
389
- else
390
- stdout_lines.each do |line|
391
- if line =~ /^([^:]+): (.+)$/
392
- key_str, value = $1, $2
393
- key = key_str.to_sym
394
- # Type-cast some values
395
- case key_str
396
- when 'date'
397
- # Date and time values
398
- # Thu Nov 23 18:43:01 UTC 2017
399
- deploy_info[key] = Time.parse(value)
400
- when 'debug'
401
- # Boolean values
402
- # Yes
403
- deploy_info[key] = (value == 'Yes')
404
- when /^diff_files_.+$/, 'services'
405
- # Array of strings
406
- # my_file.txt, other_file.txt
407
- deploy_info[key] = value.split(', ')
408
- else
409
- deploy_info[key] = value
410
- end
411
- else
412
- deploy_info[:unknown_lines] = [] unless deploy_info.key?(:unknown_lines)
413
- deploy_info[:unknown_lines] << line
414
- end
415
- end
416
- end
417
- end
418
- [
419
- node,
420
- deploy_info
421
- ]
422
- end
423
- ]
430
+ read_actions_results = @actions_executor.execute_actions(
431
+ Hash[nodes.map do |node|
432
+ master_log_plugin = @log_plugins[log_plugins_for(node).first]
433
+ master_log_plugin.respond_to?(:actions_to_read_logs) ? [node, master_log_plugin.actions_to_read_logs(node)] : nil
434
+ end.compact],
435
+ log_to_stdout: false,
436
+ concurrent: true,
437
+ timeout: 10,
438
+ progress_name: 'Read deployment logs'
439
+ )
440
+ Hash[nodes.map do |node|
441
+ [
442
+ node,
443
+ @log_plugins[log_plugins_for(node).first].logs_for(node, *(read_actions_results[node] || [nil, nil, nil]))
444
+ ]
445
+ end]
424
446
  end
425
447
 
426
448
  # Parse stdout and stderr of a given deploy run and get the list of tasks with their status
@@ -442,6 +464,35 @@ module HybridPlatformsConductor
442
464
 
443
465
  private
444
466
 
467
+ # Safe-merge 2 hashes.
468
+ # Safe-merging is done by:
469
+ # * Merging values that are hashes.
470
+ # * Reporting errors when values conflict.
471
+ # When values are conflicting, the initial hash won't modify those conflicting values and will stop the merge.
472
+ #
473
+ # Parameters::
474
+ # * *hash* (Hash): Hash to be modified merging hash_to_merge
475
+ # * *hash_to_merge* (Hash): Hash to be merged into hash
476
+ # Result::
477
+ # * nil or Array<Object>: nil in case of success, or the keys path leading to a conflicting value in case of error
478
+ def safe_merge(hash, hash_to_merge)
479
+ conflicting_path = nil
480
+ hash_to_merge.each do |key, value_to_merge|
481
+ if hash.key?(key)
482
+ if hash[key].is_a?(Hash) && value_to_merge.is_a?(Hash)
483
+ sub_conflicting_path = safe_merge(hash[key], value_to_merge)
484
+ conflicting_path = [key] + sub_conflicting_path unless sub_conflicting_path.nil?
485
+ elsif hash[key] != value_to_merge
486
+ conflicting_path = [key]
487
+ end
488
+ else
489
+ hash[key] = value_to_merge
490
+ end
491
+ break unless conflicting_path.nil?
492
+ end
493
+ conflicting_path
494
+ end
495
+
445
496
  # Get the list of retriable errors a node got from deployment logs.
446
497
  # Useful to know if an error is non-deterministic (due to external and temporary factors).
447
498
  #
@@ -494,7 +545,7 @@ module HybridPlatformsConductor
494
545
  Hash[services.map do |node, node_services|
495
546
  image_id = @nodes_handler.get_image_of(node)
496
547
  sudo = (ssh_user == 'root' ? '' : "#{@nodes_handler.sudo_on(node)} ")
497
- # Install My_company corporate certificates if present
548
+ # Install corporate certificates if present
498
549
  certificate_actions =
499
550
  if @local_environment && ENV['hpc_certificates']
500
551
  if File.exist?(ENV['hpc_certificates'])
@@ -584,47 +635,48 @@ module HybridPlatformsConductor
584
635
  # * *services* (Hash<String, Array<String>>): List of services that have been deployed, per node
585
636
  def save_logs(logs, services)
586
637
  section "Saving deployment logs for #{logs.size} nodes" do
587
- Dir.mktmpdir('hybrid_platforms_conductor-logs') do |tmp_dir|
588
- ssh_user = @actions_executor.connector(:ssh).ssh_user
589
- @actions_executor.execute_actions(
590
- Hash[logs.map do |node, (exit_status, stdout, stderr)|
591
- # Create a log file to be scp with all relevant info
592
- now = Time.now.utc
593
- log_file = "#{tmp_dir}/#{node}_#{now.strftime('%F_%H%M%S')}_#{ssh_user}"
594
- services_info = @services_handler.log_info_for(node, services[node])
595
- File.write(
596
- log_file,
597
- services_info.merge(
598
- date: now.strftime('%F %T'),
599
- user: ssh_user,
600
- debug: log_debug? ? 'Yes' : 'No',
601
- services: services[node].join(', '),
602
- exit_status: exit_status
603
- ).map { |property, value| "#{property}: #{value}" }.join("\n") +
604
- "\n===== STDOUT =====\n" +
605
- (stdout || '') +
606
- "\n===== STDERR =====\n" +
607
- (stderr || '')
608
- )
609
- [
610
- node,
611
- {
612
- remote_bash: "#{ssh_user == 'root' ? '' : "#{@nodes_handler.sudo_on(node)} "}mkdir -p /var/log/deployments",
613
- scp: {
614
- log_file => '/var/log/deployments',
615
- :sudo => ssh_user != 'root',
616
- :owner => 'root',
617
- :group => 'root'
618
- }
619
- }
620
- ]
621
- end],
622
- timeout: 10,
623
- concurrent: true,
624
- log_to_dir: nil
625
- )
626
- end
638
+ ssh_user = @actions_executor.connector(:ssh).ssh_user
639
+ @actions_executor.execute_actions(
640
+ Hash[logs.map do |node, (exit_status, stdout, stderr)|
641
+ [
642
+ node,
643
+ log_plugins_for(node).
644
+ map do |log_plugin|
645
+ @log_plugins[log_plugin].actions_to_save_logs(
646
+ node,
647
+ services[node],
648
+ @services_handler.log_info_for(node, services[node]).merge(
649
+ date: Time.now.utc.strftime('%F %T'),
650
+ user: ssh_user
651
+ ),
652
+ exit_status,
653
+ stdout,
654
+ stderr
655
+ )
656
+ end.
657
+ flatten(1)
658
+ ]
659
+ end],
660
+ timeout: 10,
661
+ concurrent: true,
662
+ log_to_dir: nil,
663
+ progress_name: 'Saving logs'
664
+ )
665
+ end
666
+ end
667
+
668
+ # Get the list of log plugins to be used for a given node
669
+ #
670
+ # Parameters::
671
+ # * *node* (String): The node for which log plugins are queried
672
+ # Result::
673
+ # * Array<Symbol>: The list of log plugins
674
+ def log_plugins_for(node)
675
+ node_log_plugins = @nodes_handler.select_confs_for_node(node, @config.deployment_logs).inject([]) do |log_plugins, deployment_logs_info|
676
+ log_plugins + deployment_logs_info[:log_plugins]
627
677
  end
678
+ node_log_plugins << :remote_fs if node_log_plugins.empty?
679
+ node_log_plugins
628
680
  end
629
681
 
630
682
  end