hybrid_platforms_conductor 33.3.0 → 33.7.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/README.md +31 -2
  4. data/docs/config_dsl.md +45 -0
  5. data/docs/plugins/cmdb/host_keys.md +3 -1
  6. data/docs/plugins/connector/ssh.md +1 -0
  7. data/lib/hybrid_platforms_conductor/actions_executor.rb +29 -1
  8. data/lib/hybrid_platforms_conductor/bitbucket.rb +134 -90
  9. data/lib/hybrid_platforms_conductor/cmd_runner.rb +4 -4
  10. data/lib/hybrid_platforms_conductor/common_config_dsl/bitbucket.rb +12 -44
  11. data/lib/hybrid_platforms_conductor/common_config_dsl/github.rb +9 -31
  12. data/lib/hybrid_platforms_conductor/config.rb +2 -0
  13. data/lib/hybrid_platforms_conductor/confluence.rb +93 -88
  14. data/lib/hybrid_platforms_conductor/connector.rb +5 -2
  15. data/lib/hybrid_platforms_conductor/credentials.rb +122 -97
  16. data/lib/hybrid_platforms_conductor/deployer.rb +7 -9
  17. data/lib/hybrid_platforms_conductor/github.rb +39 -0
  18. data/lib/hybrid_platforms_conductor/hpc_plugins/action/bash.rb +1 -1
  19. data/lib/hybrid_platforms_conductor/hpc_plugins/action/remote_bash.rb +27 -17
  20. data/lib/hybrid_platforms_conductor/hpc_plugins/cmdb/host_keys.rb +13 -12
  21. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/local.rb +6 -4
  22. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/my_connector.rb.sample +1 -1
  23. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +37 -25
  24. data/lib/hybrid_platforms_conductor/hpc_plugins/log/remote_fs.rb +5 -6
  25. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef.rb +1 -1
  26. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/docker.rb +1 -1
  27. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox.rb +7 -4
  28. data/lib/hybrid_platforms_conductor/hpc_plugins/report/confluence.rb +3 -1
  29. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +3 -2
  30. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/thycotic.rb +3 -1
  31. data/lib/hybrid_platforms_conductor/hpc_plugins/test/bitbucket_conf.rb +4 -1
  32. data/lib/hybrid_platforms_conductor/hpc_plugins/test/check_deploy_and_idempotence.rb +17 -3
  33. data/lib/hybrid_platforms_conductor/hpc_plugins/test/deploy_removes_root_access.rb +30 -10
  34. data/lib/hybrid_platforms_conductor/hpc_plugins/test/file_system.rb +1 -1
  35. data/lib/hybrid_platforms_conductor/hpc_plugins/test/github_ci.rb +4 -1
  36. data/lib/hybrid_platforms_conductor/hpc_plugins/test/hostname.rb +1 -2
  37. data/lib/hybrid_platforms_conductor/hpc_plugins/test/idempotence.rb +1 -1
  38. data/lib/hybrid_platforms_conductor/hpc_plugins/test/ip.rb +1 -2
  39. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_conf.rb +7 -3
  40. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_masters_ok.rb +8 -4
  41. data/lib/hybrid_platforms_conductor/hpc_plugins/test/local_users.rb +1 -2
  42. data/lib/hybrid_platforms_conductor/hpc_plugins/test/mounts.rb +1 -2
  43. data/lib/hybrid_platforms_conductor/hpc_plugins/test/orphan_files.rb +1 -2
  44. data/lib/hybrid_platforms_conductor/hpc_plugins/test/spectre.rb +1 -1
  45. data/lib/hybrid_platforms_conductor/hpc_plugins/test/vulnerabilities.rb +1 -2
  46. data/lib/hybrid_platforms_conductor/hpc_plugins/test_report/confluence.rb +3 -1
  47. data/lib/hybrid_platforms_conductor/logger_helpers.rb +24 -1
  48. data/lib/hybrid_platforms_conductor/test.rb +21 -7
  49. data/lib/hybrid_platforms_conductor/tests_runner.rb +7 -6
  50. data/lib/hybrid_platforms_conductor/thycotic.rb +80 -75
  51. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  52. data/spec/hybrid_platforms_conductor_test.rb +6 -0
  53. data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/bash_spec.rb +15 -0
  54. data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/remote_bash_spec.rb +32 -0
  55. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/local/remote_actions_spec.rb +87 -0
  56. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/connections_spec.rb +30 -0
  57. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/global_helpers_spec.rb +10 -0
  58. data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb +38 -0
  59. data/spec/hybrid_platforms_conductor_test/api/actions_executor/helpers_spec.rb +195 -0
  60. data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +14 -0
  61. data/spec/hybrid_platforms_conductor_test/api/config_spec.rb +11 -0
  62. data/spec/hybrid_platforms_conductor_test/api/credentials_spec.rb +251 -0
  63. data/spec/hybrid_platforms_conductor_test/api/deployer/log_plugins/remote_fs_spec.rb +215 -0
  64. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/keepass_spec.rb +280 -319
  65. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/thycotic_spec.rb +2 -2
  66. data/spec/hybrid_platforms_conductor_test/api/nodes_handler/cmdbs/host_keys_spec.rb +49 -10
  67. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/services_deployment_spec.rb +38 -0
  68. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/bitbucket_conf_spec.rb +49 -69
  69. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/github_ci_spec.rb +29 -39
  70. data/spec/hybrid_platforms_conductor_test/helpers/connector_ssh_helpers.rb +5 -3
  71. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_local_node/chef_versions.yml +3 -0
  72. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_local_node/nodes/node.json +15 -0
  73. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_local_node/policyfiles/test_policy.rb +3 -0
  74. data/spec/hybrid_platforms_conductor_test/shared_examples/deployer.rb +134 -0
  75. data/spec/hybrid_platforms_conductor_test/test_connector.rb +2 -2
  76. metadata +36 -2
@@ -59,7 +59,7 @@ module HybridPlatformsConductor
59
59
  # Raise an exception if the exit status is not the expected one.
60
60
  #
61
61
  # Parameters::
62
- # * *cmd* (String): Command to be run
62
+ # * *cmd* (String or SecretString): Command to be run
63
63
  # * *log_to_file* (String or nil): Log file capturing stdout or stderr (or nil for none). [default: nil]
64
64
  # * *log_to_stdout* (Boolean): Do we send the output to stdout? [default: true]
65
65
  # * *log_stdout_to_io* (IO or nil): IO to send command's stdout to, or nil for none. [default: nil]
@@ -108,7 +108,7 @@ module HybridPlatformsConductor
108
108
  bash_file = nil
109
109
  if force_bash
110
110
  bash_file = Tempfile.new('hpc_bash')
111
- bash_file.write(cmd)
111
+ bash_file.write(cmd.to_unprotected)
112
112
  bash_file.chmod 0o700
113
113
  bash_file.close
114
114
  cmd = "/bin/bash -c #{bash_file.path}"
@@ -136,7 +136,7 @@ module HybridPlatformsConductor
136
136
  pty: true,
137
137
  timeout: timeout,
138
138
  uuid: false
139
- ).run!(cmd) do |stdout, stderr|
139
+ ).run!(cmd.to_unprotected) do |stdout, stderr|
140
140
  stdout_queue << stdout if stdout
141
141
  stderr_queue << stderr if stderr
142
142
  end
@@ -162,7 +162,7 @@ module HybridPlatformsConductor
162
162
  log_debug "Finished in #{elapsed} seconds with exit status #{exit_status} (#{(expected_code.include?(exit_status) ? 'success'.light_green : 'failure'.light_red).bold})"
163
163
  end
164
164
  unless expected_code.include?(exit_status)
165
- error_title = "Command '#{cmd.split("\n").first}' returned error code #{exit_status} (expected #{expected_code.join(', ')})."
165
+ error_title = "Command '#{cmd.to_s.split("\n").first}' returned error code #{exit_status} (expected #{expected_code.join(', ')})."
166
166
  if no_exception
167
167
  # We consider the caller is responsible for logging what he wants about the details of the error (stdout and stderr)
168
168
  log_error error_title
@@ -1,5 +1,3 @@
1
- require 'hybrid_platforms_conductor/bitbucket'
2
-
3
1
  module HybridPlatformsConductor
4
2
 
5
3
  module CommonConfigDsl
@@ -7,12 +5,21 @@ module HybridPlatformsConductor
7
5
  # Add common Bitbucket config DSL to declare known Bitbucket repositories
8
6
  module Bitbucket
9
7
 
8
+ # List of known Bitbucket repos
9
+ # Array< Hash<Symbol, Object> >
10
+ # * *url* (String): URL to the Bitbucket server
11
+ # * *project* (String): Project name from the Bitbucket server, storing repositories
12
+ # * *repos* (Array<String> or Symbol): List of repository names from this project, or :all for all
13
+ # * *jenkins_ci_url* (String or nil): Corresponding Jenkins CI URL, or nil if none
14
+ # * *checks* (Hash<Symbol, Object>): Checks definition to be perform on those repositories (see the #for_each_bitbucket_repo to know the structure)
15
+ attr_reader :known_bitbucket_repos
16
+
10
17
  # Initialize the DSL
11
18
  def init_bitbucket
12
19
  # List of Bitbucket repositories definitions
13
20
  # Array< Hash<Symbol, Object> >
14
- # Each definition is just mapping the signature of #bitbucket_repos
15
- @bitbucket_repos = []
21
+ # Each definition is just mapping the signature of #known_bitbucket_repos
22
+ @known_bitbucket_repos = []
16
23
  end
17
24
 
18
25
  # Register new Bitbucket repositories
@@ -24,7 +31,7 @@ module HybridPlatformsConductor
24
31
  # * *jenkins_ci_url* (String or nil): Corresponding Jenkins CI URL, or nil if none [default: nil]
25
32
  # * *checks* (Hash<Symbol, Object>): Checks definition to be perform on those repositories (see the #for_each_bitbucket_repo to know the structure) [default: {}]
26
33
  def bitbucket_repos(url:, project:, repos: :all, jenkins_ci_url: nil, checks: {})
27
- @bitbucket_repos << {
34
+ @known_bitbucket_repos << {
28
35
  url: url,
29
36
  project: project,
30
37
  repos: repos,
@@ -33,45 +40,6 @@ module HybridPlatformsConductor
33
40
  }
34
41
  end
35
42
 
36
- # Iterate over each Bitbucket repository
37
- #
38
- # Parameters::
39
- # * Proc: Code called for each Bitbucket repository:
40
- # * Parameters::
41
- # * *bitbucket* (Bitbucket): The Bitbucket instance used to query the API for this repository
42
- # * *repo_info* (Hash<Symbol, Object>): The repository info:
43
- # * *name* (String): Repository name.
44
- # * *project* (String): Project name.
45
- # * *url* (String): Project Git URL.
46
- # * *jenkins_ci_url* (String or nil): Corresponding Jenkins CI URL, or nil if none.
47
- # * *checks* (Hash<Symbol, Object>): Checks to be performed on this repository:
48
- # * *branch_permissions* (Array< Hash<Symbol, Object> >): List of branch permissions to check [optional]
49
- # * *type* (String): Type of branch permissions to check. Examples of values are 'fast-forward-only', 'no-deletes', 'pull-request-only'.
50
- # * *branch* (String): Branch on which those permissions apply.
51
- # * *exempted_users* (Array<String>): List of exempted users for this permission [default: []]
52
- # * *exempted_groups* (Array<String>): List of exempted groups for this permission [default: []]
53
- # * *exempted_keys* (Array<String>): List of exempted access keys for this permission [default: []]
54
- # * *pr_settings* (Hash<Symbol, Object>): PR specific settings to check [optional]
55
- # * *required_approvers* (Integer): Number of required approvers [optional]
56
- # * *required_builds* (Integer): Number of required successful builds [optional]
57
- # * *default_merge_strategy* (String): Name of the default merge strategy. Example: 'rebase-no-ff' [optional]
58
- # * *mandatory_default_reviewers* (Array<String>): List of mandatory reviewers to check [default: []]
59
- def for_each_bitbucket_repo
60
- @bitbucket_repos.each do |bitbucket_repo_info|
61
- HybridPlatformsConductor::Bitbucket.with_bitbucket(bitbucket_repo_info[:url], @logger, @logger_stderr) do |bitbucket|
62
- (bitbucket_repo_info[:repos] == :all ? bitbucket.repos(bitbucket_repo_info[:project])['values'].map { |repo_info| repo_info['slug'] } : bitbucket_repo_info[:repos]).each do |name|
63
- yield bitbucket, {
64
- name: name,
65
- project: bitbucket_repo_info[:project],
66
- url: "#{bitbucket_repo_info[:url]}/scm/#{bitbucket_repo_info[:project].downcase}/#{name}.git",
67
- jenkins_ci_url: bitbucket_repo_info[:jenkins_ci_url].nil? ? nil : "#{bitbucket_repo_info[:jenkins_ci_url]}/job/#{name}",
68
- checks: bitbucket_repo_info[:checks]
69
- }
70
- end
71
- end
72
- end
73
- end
74
-
75
43
  end
76
44
 
77
45
  end
@@ -1,6 +1,3 @@
1
- require 'octokit'
2
- require 'hybrid_platforms_conductor/credentials'
3
-
4
1
  module HybridPlatformsConductor
5
2
 
6
3
  module CommonConfigDsl
@@ -8,12 +5,19 @@ module HybridPlatformsConductor
8
5
  # Add common Github config DSL to declare known Github repositories
9
6
  module Github
10
7
 
8
+ # List of Github repositories
9
+ # Array< Hash<Symbol, Object> >
10
+ # * *user* (String): User or organization name, storing repositories
11
+ # * *url* (String): URL to the Github API
12
+ # * *repos* (Array<String> or Symbol): List of repository names from this project, or :all for all
13
+ attr_reader :known_github_repos
14
+
11
15
  # Initialize the DSL
12
16
  def init_github
13
17
  # List of Github repositories definitions
14
18
  # Array< Hash<Symbol, Object> >
15
19
  # Each definition is just mapping the signature of #github_repos
16
- @github_repos = []
20
+ @known_github_repos = []
17
21
  end
18
22
 
19
23
  # Register new Github repositories
@@ -23,39 +27,13 @@ module HybridPlatformsConductor
23
27
  # * *url* (String): URL to the Github API [default: 'https://api.github.com']
24
28
  # * *repos* (Array<String> or Symbol): List of repository names from this project, or :all for all [default: :all]
25
29
  def github_repos(user:, url: 'https://api.github.com', repos: :all)
26
- @github_repos << {
30
+ @known_github_repos << {
27
31
  url: url,
28
32
  user: user,
29
33
  repos: repos
30
34
  }
31
35
  end
32
36
 
33
- # Iterate over each Github repository
34
- #
35
- # Parameters::
36
- # * Proc: Code called for each Github repository:
37
- # * Parameters::
38
- # * *github* (Octokit::Client): The client instance accessing the Github API
39
- # * *repo_info* (Hash<Symbol, Object>): The repository info:
40
- # * *name* (String): Repository name.
41
- # * *slug* (String): Repository slug.
42
- def for_each_github_repo
43
- @github_repos.each do |repo_info|
44
- Octokit.configure do |c|
45
- c.api_endpoint = repo_info[:url]
46
- end
47
- Credentials.with_credentials_for(:github, @logger, @logger_stderr, url: repo_info[:url]) do |_github_user, github_token|
48
- client = Octokit::Client.new(access_token: github_token)
49
- (repo_info[:repos] == :all ? client.repositories(repo_info[:user]).map { |repo| repo[:name] } : repo_info[:repos]).each do |name|
50
- yield client, {
51
- name: name,
52
- slug: "#{repo_info[:user]}/#{name}"
53
- }
54
- end
55
- end
56
- end
57
- end
58
-
59
37
  end
60
38
 
61
39
  end
@@ -34,6 +34,8 @@ module HybridPlatformsConductor
34
34
  end
35
35
  @mixin_initializers = []
36
36
 
37
+ expose :log_debug?
38
+
37
39
  # Directory of the definition of the platforms
38
40
  # String
39
41
  attr_reader :hybrid_platforms_dir
@@ -7,111 +7,116 @@ require 'hybrid_platforms_conductor/credentials'
7
7
 
8
8
  module HybridPlatformsConductor
9
9
 
10
- # Object used to access Confluence API
11
- class Confluence
10
+ # Mixin used to access Confluence API
11
+ module Confluence
12
12
 
13
- include LoggerHelpers
13
+ include Credentials
14
14
 
15
15
  # Provide a Confluence connector, and make sure the password is being cleaned when exiting.
16
16
  #
17
17
  # Parameters::
18
18
  # * *confluence_url* (String): The Confluence URL
19
- # * *logger* (Logger): Logger to be used
20
- # * *logger_stderr* (Logger): Logger to be used for stderr
21
19
  # * Proc: Code called with the Confluence instance.
22
- # * *confluence* (Confluence): The Confluence instance to use.
23
- def self.with_confluence(confluence_url, logger, logger_stderr)
24
- Credentials.with_credentials_for(:confluence, logger, logger_stderr, url: confluence_url) do |confluence_user, confluence_password|
25
- yield Confluence.new(confluence_url, confluence_user, confluence_password, logger: logger, logger_stderr: logger_stderr)
20
+ # * *confluence* (ConfluenceApi): The Confluence instance to use.
21
+ def with_confluence(confluence_url)
22
+ with_credentials_for(:confluence, resource: confluence_url) do |confluence_user, confluence_password|
23
+ yield ConfluenceApi.new(confluence_url, confluence_user, confluence_password, logger: @logger, logger_stderr: @logger_stderr)
26
24
  end
27
25
  end
28
26
 
29
- # Constructor
30
- #
31
- # Parameters::
32
- # * *confluence_url* (String): The Confluence URL
33
- # * *confluence_user_name* (String): Confluence user name to be used when querying the API
34
- # * *confluence_password* (String): Confluence password to be used when querying the API
35
- # * *logger* (Logger): Logger to be used [default = Logger.new(STDOUT)]
36
- # * *logger_stderr* (Logger): Logger to be used for stderr [default = Logger.new(STDERR)]
37
- def initialize(confluence_url, confluence_user_name, confluence_password, logger: Logger.new($stdout), logger_stderr: Logger.new($stderr))
38
- init_loggers(logger, logger_stderr)
39
- @confluence_url = confluence_url
40
- @confluence_user_name = confluence_user_name
41
- @confluence_password = confluence_password
42
- end
27
+ # Provide an API access on Confluence
28
+ class ConfluenceApi
43
29
 
44
- # Return a Confluence storage format content from a page ID
45
- #
46
- # Parameters::
47
- # * *page_id* (String): Confluence page ID
48
- # Result::
49
- # * Nokogiri::HTML: Storage format content, as a Nokogiri object
50
- def page_storage_format(page_id)
51
- Nokogiri::HTML(call_api("plugins/viewstorage/viewpagestorage.action?pageId=#{page_id}").body)
52
- end
30
+ include LoggerHelpers
53
31
 
54
- # Return some info of a given page ID
55
- #
56
- # Parameters::
57
- # * *page_id* (String): Confluence page ID
58
- # Result::
59
- # * Hash: Page information, as returned by the Confluence API
60
- def page_info(page_id)
61
- JSON.parse(call_api("rest/api/content/#{page_id}").body)
62
- end
32
+ # Constructor
33
+ #
34
+ # Parameters::
35
+ # * *confluence_url* (String): The Confluence URL
36
+ # * *confluence_user_name* (String): Confluence user name to be used when querying the API
37
+ # * *confluence_password* (SecretString): Confluence password to be used when querying the API
38
+ # * *logger* (Logger): Logger to be used [default = Logger.new(STDOUT)]
39
+ # * *logger_stderr* (Logger): Logger to be used for stderr [default = Logger.new(STDERR)]
40
+ def initialize(confluence_url, confluence_user_name, confluence_password, logger: Logger.new($stdout), logger_stderr: Logger.new($stderr))
41
+ init_loggers(logger, logger_stderr)
42
+ @confluence_url = confluence_url
43
+ @confluence_user_name = confluence_user_name
44
+ @confluence_password = confluence_password
45
+ end
63
46
 
64
- # Update a Confluence page to a new content.
65
- #
66
- # Parameters::
67
- # * *page_id* (String): Confluence page ID
68
- # * *content* (String): New content
69
- # * *version* (String or nil): New version, or nil to automatically increase last existing version [default: nil]
70
- def update_page(page_id, content, version: nil)
71
- info = page_info(page_id)
72
- version = info['version']['number'] + 1 if version.nil?
73
- log_debug "Update Confluence page #{page_id}..."
74
- call_api("rest/api/content/#{page_id}", :put) do |request|
75
- request['Content-Type'] = 'application/json'
76
- request.body = {
77
- type: 'page',
78
- title: info['title'],
79
- body: {
80
- storage: {
81
- value: content,
82
- representation: 'storage'
83
- }
84
- },
85
- version: { number: version }
86
- }.to_json
47
+ # Return a Confluence storage format content from a page ID
48
+ #
49
+ # Parameters::
50
+ # * *page_id* (String): Confluence page ID
51
+ # Result::
52
+ # * Nokogiri::HTML: Storage format content, as a Nokogiri object
53
+ def page_storage_format(page_id)
54
+ Nokogiri::HTML(call_api("plugins/viewstorage/viewpagestorage.action?pageId=#{page_id}").body)
87
55
  end
88
- end
89
56
 
90
- private
57
+ # Return some info of a given page ID
58
+ #
59
+ # Parameters::
60
+ # * *page_id* (String): Confluence page ID
61
+ # Result::
62
+ # * Hash: Page information, as returned by the Confluence API
63
+ def page_info(page_id)
64
+ JSON.parse(call_api("rest/api/content/#{page_id}").body)
65
+ end
91
66
 
92
- # Call the Confluence API for a given URL and HTTP verb.
93
- # Provide a simple way to tweak the request with an optional proc.
94
- # Automatically handles authentication, base URL and error handling.
95
- #
96
- # Parameters::
97
- # * *api_path* (String): The API path to query
98
- # * *http_method* (Symbol): HTTP method to be used to create the request [default = :get]
99
- # * Proc: Optional code called to alter the request
100
- # * Parameters::
101
- # * *request* (Net::HTTPRequest): The request
102
- # Result::
103
- # * Net::HTTPResponse: The corresponding response
104
- def call_api(api_path, http_method = :get)
105
- response = nil
106
- page_url = URI.parse("#{@confluence_url}/#{api_path}")
107
- Net::HTTP.start(page_url.host, page_url.port, use_ssl: true) do |http|
108
- request = Net::HTTP.const_get(http_method.to_s.capitalize.to_sym).new(page_url.request_uri)
109
- request.basic_auth @confluence_user_name, @confluence_password
110
- yield request if block_given?
111
- response = http.request(request)
112
- raise "Confluence page API request on #{page_url} returned an error: #{response.code}\n#{response.body}\n===== Request body =====\n#{request.body}" unless response.is_a?(Net::HTTPSuccess)
67
+ # Update a Confluence page to a new content.
68
+ #
69
+ # Parameters::
70
+ # * *page_id* (String): Confluence page ID
71
+ # * *content* (String): New content
72
+ # * *version* (String or nil): New version, or nil to automatically increase last existing version [default: nil]
73
+ def update_page(page_id, content, version: nil)
74
+ info = page_info(page_id)
75
+ version = info['version']['number'] + 1 if version.nil?
76
+ log_debug "Update Confluence page #{page_id}..."
77
+ call_api("rest/api/content/#{page_id}", :put) do |request|
78
+ request['Content-Type'] = 'application/json'
79
+ request.body = {
80
+ type: 'page',
81
+ title: info['title'],
82
+ body: {
83
+ storage: {
84
+ value: content,
85
+ representation: 'storage'
86
+ }
87
+ },
88
+ version: { number: version }
89
+ }.to_json
90
+ end
113
91
  end
114
- response
92
+
93
+ private
94
+
95
+ # Call the Confluence API for a given URL and HTTP verb.
96
+ # Provide a simple way to tweak the request with an optional proc.
97
+ # Automatically handles authentication, base URL and error handling.
98
+ #
99
+ # Parameters::
100
+ # * *api_path* (String): The API path to query
101
+ # * *http_method* (Symbol): HTTP method to be used to create the request [default = :get]
102
+ # * Proc: Optional code called to alter the request
103
+ # * Parameters::
104
+ # * *request* (Net::HTTPRequest): The request
105
+ # Result::
106
+ # * Net::HTTPResponse: The corresponding response
107
+ def call_api(api_path, http_method = :get)
108
+ response = nil
109
+ page_url = URI.parse("#{@confluence_url}/#{api_path}")
110
+ Net::HTTP.start(page_url.host, page_url.port, use_ssl: true) do |http|
111
+ request = Net::HTTP.const_get(http_method.to_s.capitalize.to_sym).new(page_url.request_uri)
112
+ request.basic_auth @confluence_user_name, @confluence_password&.to_unprotected
113
+ yield request if block_given?
114
+ response = http.request(request)
115
+ raise "Confluence page API request on #{page_url} returned an error: #{response.code}\n#{response.body}\n===== Request body =====\n#{request.body}" unless response.is_a?(Net::HTTPSuccess)
116
+ end
117
+ response
118
+ end
119
+
115
120
  end
116
121
 
117
122
  end
@@ -14,16 +14,19 @@ module HybridPlatformsConductor
14
14
  # * *config* (Config): Config to be used. [default: Config.new]
15
15
  # * *cmd_runner* (CmdRunner): Command executor to be used. [default: CmdRunner.new]
16
16
  # * *nodes_handler* (NodesHandler): NodesHandler to be used. [default: NodesHandler.new]
17
+ # * *actions_executor* (ActionsExecutor): ActionsExecutor to be used. [default: ActionsExecutor.new]
17
18
  def initialize(
18
19
  logger: Logger.new($stdout),
19
20
  logger_stderr: Logger.new($stderr),
20
21
  config: Config.new,
21
22
  cmd_runner: CmdRunner.new,
22
- nodes_handler: NodesHandler.new
23
+ nodes_handler: NodesHandler.new,
24
+ actions_executor: ActionsExecutor.new
23
25
  )
24
26
  super(logger: logger, logger_stderr: logger_stderr, config: config)
25
27
  @cmd_runner = cmd_runner
26
28
  @nodes_handler = nodes_handler
29
+ @actions_executor = actions_executor
27
30
  # If the connector has an initializer, use it
28
31
  init if respond_to?(:init)
29
32
  end
@@ -67,7 +70,7 @@ module HybridPlatformsConductor
67
70
  # Handle the redirection of standard output and standard error to file and stdout depending on the context of the run.
68
71
  #
69
72
  # Parameters::
70
- # * *cmd* (String): The command to be run
73
+ # * *cmd* (String or SecretString): The command to be run
71
74
  # * *force_bash* (Boolean): If true, then make sure command is invoked with bash instead of sh [default: false]
72
75
  # Result::
73
76
  # * Integer: Exit code
@@ -1,4 +1,5 @@
1
1
  require 'netrc'
2
+ require 'secret_string'
2
3
  require 'uri'
3
4
  require 'hybrid_platforms_conductor/logger_helpers'
4
5
 
@@ -7,122 +8,146 @@ module HybridPlatformsConductor
7
8
  # Give a secured and harmonized way to access credentials for a given service.
8
9
  # It makes sure to remove passwords from memory for hardened security (this way if a vulnerability allows an attacker to dump the memory it won't get passwords).
9
10
  # It gets credentials from the following sources:
11
+ # * Configuration
10
12
  # * Environment variables
11
13
  # * Netrc file
12
- class Credentials
14
+ module Credentials
13
15
 
14
- include LoggerHelpers
16
+ # Extend the Config DSL
17
+ module ConfigDSLExtension
18
+
19
+ # List of credentials. Each info has the following properties:
20
+ # * *credential_id* (Symbol): Credential ID this rule applies to
21
+ # * *resource* (Regexp): Resource filtering for this rule
22
+ # * *provider* (Proc): The code providing the credentials:
23
+ # * Parameters::
24
+ # * *resource* (String or nil): The resource for which we want credentials, or nil if none
25
+ # * *requester* (Proc): Code to be called to give credentials to:
26
+ # * Parameters::
27
+ # * *user* (String or nil): The user name, or nil if none
28
+ # * *password* (String or nil): The password, or nil if none
29
+ attr_reader :credentials
30
+
31
+ # Mixin initializer
32
+ def init_credentials_config
33
+ @credentials = []
34
+ end
35
+
36
+ # Define a credentials provider
37
+ #
38
+ # Parameters::
39
+ # * *credential_id* (Symbol): Credential ID this rule applies to
40
+ # * *resource* (String or Regexp): Resource filtering for this rule [default: /.*/]
41
+ # * *provider* (Proc): The code providing the credentials:
42
+ # * Parameters::
43
+ # * *resource* (String or nil): The resource for which we want credentials, or nil if none
44
+ # * *requester* (Proc): Code to be called to give credentials to:
45
+ # * Parameters::
46
+ # * *user* (String or nil): The user name, or nil if none
47
+ # * *password* (String or nil): The password, or nil if none
48
+ def credentials_for(credential_id, resource: /.*/, &provider)
49
+ @credentials << {
50
+ credential_id: credential_id,
51
+ resource: resource.is_a?(String) ? /^#{Regexp.escape(resource)}$/ : resource,
52
+ provider: provider
53
+ }
54
+ end
55
+
56
+ end
57
+
58
+ Config.extend_config_dsl_with ConfigDSLExtension, :init_credentials_config
15
59
 
16
60
  # Get access to credentials and make sure they are wiped out from memory when client code ends.
17
61
  # To ensure password safety, never store the password in a scope beyond the client code's Proc.
18
62
  #
19
63
  # Parameters::
20
64
  # * *id* (Symbol): Credential ID
21
- # * *logger* (Logger): Logger to be used
22
- # * *logger_stderr* (Logger): Logger to be used for stderr
23
- # * *url* (String or nil): The URL for which we want the credentials, or nil if not associated to a URL [default: nil]
65
+ # * *resource* (String or nil): The resource for which we want the credentials, or nil if not associated to a resource [default: nil]
24
66
  # * Proc: Client code called with credentials provided
25
67
  # * Parameters::
26
68
  # * *user* (String or nil): User name, or nil if none
27
- # * *password* (String or nil): Password, or nil if none.
28
- # !!! Never store this password in a scope broader than the client code itself !!!
29
- def self.with_credentials_for(id, logger, logger_stderr, url: nil)
30
- credentials = Credentials.new(id, url: url, logger: logger, logger_stderr: logger_stderr)
31
- begin
32
- yield credentials.user, credentials.password
33
- ensure
34
- credentials.clear_password
35
- end
36
- end
69
+ # * *password* (SecretString or nil): Password, or nil if none.
70
+ # !!! Never clone this password in a scope broader than the client code itself !!!
71
+ def with_credentials_for(id, resource: nil)
72
+ # Get the credentials provider
73
+ provider = nil
37
74
 
38
- # Constructor
39
- #
40
- # Parameters::
41
- # * *id* (Symbol): Credential ID
42
- # * *url* (String or nil): The URL for which we want the credentials, or nil if not associated to a URL [default: nil]
43
- # * *logger* (Logger): Logger to be used [default = Logger.new(STDOUT)]
44
- # * *logger_stderr* (Logger): Logger to be used for stderr [default = Logger.new(STDERR)]
45
- def initialize(id, url: nil, logger: Logger.new($stdout), logger_stderr: Logger.new($stderr))
46
- init_loggers(logger, logger_stderr)
47
- @id = id
48
- @url = url
49
- @user = nil
50
- @password = nil
51
- @retrieved = false
52
- end
53
-
54
- # Provide a helper to clear password from memory for security.
55
- # To be used when the client knows it won't use the password anymore.
56
- def clear_password
57
- @password&.replace('gotyou!' * 100)
58
- GC.start
59
- end
60
-
61
- # Get the associated user
62
- #
63
- # Result::
64
- # * String or nil: The user name, or nil if none
65
- def user
66
- retrieve_credentials
67
- @user
68
- end
69
-
70
- # Get the associated password
71
- #
72
- # Result::
73
- # * String or nil: The password, or nil if none
74
- def password
75
- retrieve_credentials
76
- @password
77
- end
78
-
79
- private
80
-
81
- # Retrieve credentials in @user and @password.
82
- # Do it only once.
83
- # Make sure the retrieved credentials are not linked to other objects in memory, so that we can remove any other trace of secrets.
84
- def retrieve_credentials
85
- return if @retrieved
75
+ # Check configuration
76
+ # Take the last matching provider, this way we can define several providers for resources matched in a increasingly refined way.
77
+ @config.credentials.each do |credentials_info|
78
+ provider = credentials_info[:provider] if credentials_info[:credential_id] == id && (
79
+ (resource.nil? && credentials_info[:resource] == /.*/) || credentials_info[:resource] =~ resource
80
+ )
81
+ end
86
82
 
87
- # Check environment variables
88
- @user = ENV["hpc_user_for_#{@id}"].dup
89
- @password = ENV["hpc_password_for_#{@id}"].dup
90
- if @user.nil? || @user.empty? || @password.nil? || @password.empty?
91
- log_debug "[ Credentials for #{@id} ] - Credentials not found from environment variables."
92
- if @url.nil?
93
- log_debug "[ Credentials for #{@id} ] - No URL associated to this credentials, so .netrc can't be used."
94
- else
95
- # Check Netrc
96
- netrc = ::Netrc.read
97
- begin
98
- netrc_user, netrc_password = netrc[URI.parse(@url).host.downcase]
99
- if netrc_user.nil?
100
- log_debug "[ Credentials for #{@id} ] - No credentials retrieved from .netrc."
101
- # TODO: Add more credentials source if needed here
102
- log_warn "[ Credentials for #{@id} ] - Unable to get credentials for #{@id} (URL: #{@url})."
103
- else
104
- @user = netrc_user.dup
105
- @password = netrc_password.dup
106
- log_debug "[ Credentials for #{@id} ] - Credentials retrieved from .netrc using #{@url}."
107
- end
108
- ensure
109
- # Make sure the password does not stay in Netrc memory
110
- # Wipe out any memory trace that might contain passwords in clear
111
- netrc.instance_variable_get(:@data).each do |data_line|
112
- data_line.each do |data_string|
113
- data_string.replace('GotYou!!!' * 100)
83
+ provider ||= proc do |requested_resource, requester|
84
+ # Check environment variables
85
+ user = ENV["hpc_user_for_#{id}"]
86
+ # Clone the password as we are going to treat it as a secret string that will be wiped out
87
+ password = ENV["hpc_password_for_#{id}"].dup
88
+ if user.nil? || user.empty? || password.nil? || password.empty?
89
+ log_debug "[ Credentials for #{id} ] - Credentials not found from environment variables."
90
+ if requested_resource.nil?
91
+ log_debug "[ Credentials for #{id} ] - No resource associated to this credentials, so .netrc can't be used."
92
+ else
93
+ # Check Netrc
94
+ netrc = ::Netrc.read
95
+ begin
96
+ netrc_user, netrc_password = netrc[
97
+ begin
98
+ URI.parse(requested_resource).host.downcase
99
+ rescue URI::InvalidURIError
100
+ requested_resource
101
+ end
102
+ ]
103
+ if netrc_user.nil?
104
+ log_debug "[ Credentials for #{id} ] - No credentials retrieved from .netrc."
105
+ # TODO: Add more credentials source if needed here
106
+ log_warn "[ Credentials for #{id} ] - Unable to get credentials for #{id} (Resource: #{requested_resource})."
107
+ else
108
+ # Clone in memory as we are going to wipe out ::Netrc's memory
109
+ user = netrc_user.dup
110
+ password = netrc_password.dup
111
+ log_debug "[ Credentials for #{id} ] - Credentials retrieved from .netrc using #{requested_resource}."
112
+ end
113
+ ensure
114
+ # Make sure the password does not stay in Netrc memory
115
+ # Wipe out any memory trace that might contain passwords in clear
116
+ netrc.instance_variable_get(:@data).each do |data_line|
117
+ data_line.each do |data_string|
118
+ data_string.replace('GotYou!!!' * 100)
119
+ end
114
120
  end
115
121
  end
116
- # We don this assignment on purpose so that GC can remove sensitive data later
117
- # rubocop:disable Lint/UselessAssignment
118
- netrc = nil
119
- # rubocop:enable Lint/UselessAssignment
122
+ end
123
+ else
124
+ log_debug "[ Credentials for #{id} ] - Credentials retrieved from environment variables."
125
+ end
126
+ if password.nil?
127
+ requester.call user, password
128
+ else
129
+ SecretString.protect(password) do |secret_password|
130
+ requester.call user, secret_password
120
131
  end
121
132
  end
122
- else
123
- log_debug "[ Credentials for #{@id} ] - Credentials retrieved from environment variables."
124
133
  end
125
- GC.start
134
+
135
+ requester_called = false
136
+ provider.call(
137
+ resource,
138
+ proc do |user, password|
139
+ requester_called = true
140
+ if password.is_a?(String)
141
+ SecretString.protect(password) do |secret_password|
142
+ yield user, secret_password
143
+ end
144
+ else
145
+ yield user, password
146
+ end
147
+ end
148
+ )
149
+
150
+ raise "Requester not called by the credentials provider for #{id} (resource: #{resource}) - Please check the credentials_for code in your configuration." unless requester_called
126
151
  end
127
152
 
128
153
  end