hybrid_platforms_conductor 33.3.0 → 33.7.0

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