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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +31 -2
- data/docs/config_dsl.md +45 -0
- data/docs/plugins/cmdb/host_keys.md +3 -1
- data/docs/plugins/connector/ssh.md +1 -0
- data/lib/hybrid_platforms_conductor/actions_executor.rb +29 -1
- data/lib/hybrid_platforms_conductor/bitbucket.rb +134 -90
- data/lib/hybrid_platforms_conductor/cmd_runner.rb +4 -4
- data/lib/hybrid_platforms_conductor/common_config_dsl/bitbucket.rb +12 -44
- data/lib/hybrid_platforms_conductor/common_config_dsl/github.rb +9 -31
- data/lib/hybrid_platforms_conductor/config.rb +2 -0
- data/lib/hybrid_platforms_conductor/confluence.rb +93 -88
- data/lib/hybrid_platforms_conductor/connector.rb +5 -2
- data/lib/hybrid_platforms_conductor/credentials.rb +122 -97
- data/lib/hybrid_platforms_conductor/deployer.rb +7 -9
- data/lib/hybrid_platforms_conductor/github.rb +39 -0
- data/lib/hybrid_platforms_conductor/hpc_plugins/action/bash.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/action/remote_bash.rb +27 -17
- data/lib/hybrid_platforms_conductor/hpc_plugins/cmdb/host_keys.rb +13 -12
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/local.rb +6 -4
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/my_connector.rb.sample +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/connector/ssh.rb +37 -25
- data/lib/hybrid_platforms_conductor/hpc_plugins/log/remote_fs.rb +5 -6
- data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/docker.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox.rb +7 -4
- data/lib/hybrid_platforms_conductor/hpc_plugins/report/confluence.rb +3 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +3 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/thycotic.rb +3 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/bitbucket_conf.rb +4 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/check_deploy_and_idempotence.rb +17 -3
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/deploy_removes_root_access.rb +30 -10
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/file_system.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/github_ci.rb +4 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/hostname.rb +1 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/idempotence.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/ip.rb +1 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_conf.rb +7 -3
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_masters_ok.rb +8 -4
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/local_users.rb +1 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/mounts.rb +1 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/orphan_files.rb +1 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/spectre.rb +1 -1
- data/lib/hybrid_platforms_conductor/hpc_plugins/test/vulnerabilities.rb +1 -2
- data/lib/hybrid_platforms_conductor/hpc_plugins/test_report/confluence.rb +3 -1
- data/lib/hybrid_platforms_conductor/logger_helpers.rb +24 -1
- data/lib/hybrid_platforms_conductor/test.rb +21 -7
- data/lib/hybrid_platforms_conductor/tests_runner.rb +7 -6
- data/lib/hybrid_platforms_conductor/thycotic.rb +80 -75
- data/lib/hybrid_platforms_conductor/version.rb +1 -1
- data/spec/hybrid_platforms_conductor_test.rb +6 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/bash_spec.rb +15 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/actions/remote_bash_spec.rb +32 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/local/remote_actions_spec.rb +87 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/connections_spec.rb +30 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/global_helpers_spec.rb +10 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/connectors/ssh/remote_actions_spec.rb +38 -0
- data/spec/hybrid_platforms_conductor_test/api/actions_executor/helpers_spec.rb +195 -0
- data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +14 -0
- data/spec/hybrid_platforms_conductor_test/api/config_spec.rb +11 -0
- data/spec/hybrid_platforms_conductor_test/api/credentials_spec.rb +251 -0
- data/spec/hybrid_platforms_conductor_test/api/deployer/log_plugins/remote_fs_spec.rb +215 -0
- data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/keepass_spec.rb +280 -319
- data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/thycotic_spec.rb +2 -2
- data/spec/hybrid_platforms_conductor_test/api/nodes_handler/cmdbs/host_keys_spec.rb +49 -10
- data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/services_deployment_spec.rb +38 -0
- data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/bitbucket_conf_spec.rb +49 -69
- data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/github_ci_spec.rb +29 -39
- data/spec/hybrid_platforms_conductor_test/helpers/connector_ssh_helpers.rb +5 -3
- data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_local_node/chef_versions.yml +3 -0
- data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_local_node/nodes/node.json +15 -0
- data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_local_node/policyfiles/test_policy.rb +3 -0
- data/spec/hybrid_platforms_conductor_test/shared_examples/deployer.rb +134 -0
- data/spec/hybrid_platforms_conductor_test/test_connector.rb +2 -2
- 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 #
|
15
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
@@ -7,111 +7,116 @@ require 'hybrid_platforms_conductor/credentials'
|
|
7
7
|
|
8
8
|
module HybridPlatformsConductor
|
9
9
|
|
10
|
-
#
|
11
|
-
|
10
|
+
# Mixin used to access Confluence API
|
11
|
+
module Confluence
|
12
12
|
|
13
|
-
include
|
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* (
|
23
|
-
def
|
24
|
-
|
25
|
-
yield
|
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
|
-
#
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
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
|
-
|
14
|
+
module Credentials
|
13
15
|
|
14
|
-
|
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
|
-
# * *
|
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* (
|
28
|
-
# !!! Never
|
29
|
-
def
|
30
|
-
|
31
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
if
|
93
|
-
log_debug "[ Credentials for #{
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|