hybrid_platforms_conductor 32.16.3 → 33.0.0

Sign up to get free protection for your applications and to get access to all the features.
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