hybrid_platforms_conductor 33.3.0 → 33.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -0
  3. data/README.md +31 -2
  4. data/docs/config_dsl.md +43 -0
  5. data/lib/hybrid_platforms_conductor/bitbucket.rb +134 -90
  6. data/lib/hybrid_platforms_conductor/common_config_dsl/bitbucket.rb +12 -44
  7. data/lib/hybrid_platforms_conductor/common_config_dsl/github.rb +9 -31
  8. data/lib/hybrid_platforms_conductor/confluence.rb +93 -88
  9. data/lib/hybrid_platforms_conductor/credentials.rb +112 -95
  10. data/lib/hybrid_platforms_conductor/deployer.rb +2 -2
  11. data/lib/hybrid_platforms_conductor/github.rb +39 -0
  12. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/proxmox.rb +4 -2
  13. data/lib/hybrid_platforms_conductor/hpc_plugins/report/confluence.rb +3 -1
  14. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/keepass.rb +2 -1
  15. data/lib/hybrid_platforms_conductor/hpc_plugins/secrets_reader/thycotic.rb +3 -1
  16. data/lib/hybrid_platforms_conductor/hpc_plugins/test/bitbucket_conf.rb +4 -1
  17. data/lib/hybrid_platforms_conductor/hpc_plugins/test/github_ci.rb +4 -1
  18. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_conf.rb +6 -2
  19. data/lib/hybrid_platforms_conductor/hpc_plugins/test/jenkins_ci_masters_ok.rb +6 -2
  20. data/lib/hybrid_platforms_conductor/hpc_plugins/test_report/confluence.rb +3 -1
  21. data/lib/hybrid_platforms_conductor/logger_helpers.rb +7 -1
  22. data/lib/hybrid_platforms_conductor/thycotic.rb +80 -75
  23. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  24. data/spec/hybrid_platforms_conductor_test.rb +6 -0
  25. data/spec/hybrid_platforms_conductor_test/api/credentials_spec.rb +247 -0
  26. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/keepass_spec.rb +280 -319
  27. data/spec/hybrid_platforms_conductor_test/api/deployer/secrets_reader_plugins/thycotic_spec.rb +2 -2
  28. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/bitbucket_conf_spec.rb +49 -69
  29. data/spec/hybrid_platforms_conductor_test/api/tests_runner/test_plugins/github_ci_spec.rb +29 -39
  30. metadata +18 -2
@@ -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
@@ -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* (String): 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
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
@@ -7,122 +7,139 @@ module HybridPlatformsConductor
7
7
  # Give a secured and harmonized way to access credentials for a given service.
8
8
  # 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
9
  # It gets credentials from the following sources:
10
+ # * Configuration
10
11
  # * Environment variables
11
12
  # * Netrc file
12
- class Credentials
13
+ module Credentials
13
14
 
14
- include LoggerHelpers
15
+ # Extend the Config DSL
16
+ module ConfigDSLExtension
17
+
18
+ # List of credentials. Each info has the following properties:
19
+ # * *credential_id* (Symbol): Credential ID this rule applies to
20
+ # * *resource* (Regexp): Resource filtering for this rule
21
+ # * *provider* (Proc): The code providing the credentials:
22
+ # * Parameters::
23
+ # * *resource* (String or nil): The resource for which we want credentials, or nil if none
24
+ # * *requester* (Proc): Code to be called to give credentials to:
25
+ # * Parameters::
26
+ # * *user* (String or nil): The user name, or nil if none
27
+ # * *password* (String or nil): The password, or nil if none
28
+ attr_reader :credentials
29
+
30
+ # Mixin initializer
31
+ def init_credentials_config
32
+ @credentials = []
33
+ end
34
+
35
+ # Define a credentials provider
36
+ #
37
+ # Parameters::
38
+ # * *credential_id* (Symbol): Credential ID this rule applies to
39
+ # * *resource* (String or Regexp): Resource filtering for this rule [default: /.*/]
40
+ # * *provider* (Proc): The code providing the credentials:
41
+ # * Parameters::
42
+ # * *resource* (String or nil): The resource for which we want credentials, or nil if none
43
+ # * *requester* (Proc): Code to be called to give credentials to:
44
+ # * Parameters::
45
+ # * *user* (String or nil): The user name, or nil if none
46
+ # * *password* (String or nil): The password, or nil if none
47
+ def credentials_for(credential_id, resource: /.*/, &provider)
48
+ @credentials << {
49
+ credential_id: credential_id,
50
+ resource: resource.is_a?(String) ? /^#{Regexp.escape(resource)}$/ : resource,
51
+ provider: provider
52
+ }
53
+ end
54
+
55
+ end
56
+
57
+ Config.extend_config_dsl_with ConfigDSLExtension, :init_credentials_config
15
58
 
16
59
  # Get access to credentials and make sure they are wiped out from memory when client code ends.
17
60
  # To ensure password safety, never store the password in a scope beyond the client code's Proc.
18
61
  #
19
62
  # Parameters::
20
63
  # * *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]
64
+ # * *resource* (String or nil): The resource for which we want the credentials, or nil if not associated to a resource [default: nil]
24
65
  # * Proc: Client code called with credentials provided
25
66
  # * Parameters::
26
67
  # * *user* (String or nil): User name, or nil if none
27
68
  # * *password* (String or nil): Password, or nil if none.
28
69
  # !!! 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
37
-
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
70
+ def with_credentials_for(id, resource: nil)
71
+ # Get the credentials provider
72
+ provider = nil
80
73
 
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
74
+ # Check configuration
75
+ # Take the last matching provider, this way we can define several providers for resources matched in a increasingly refined way.
76
+ @config.credentials.each do |credentials_info|
77
+ provider = credentials_info[:provider] if credentials_info[:credential_id] == id && (
78
+ (resource.nil? && credentials_info[:resource] == /.*/) || credentials_info[:resource] =~ resource
79
+ )
80
+ end
86
81
 
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)
82
+ provider ||= proc do |requested_resource, requester|
83
+ # Check environment variables
84
+ user = ENV["hpc_user_for_#{id}"].dup
85
+ password = ENV["hpc_password_for_#{id}"].dup
86
+ if user.nil? || user.empty? || password.nil? || password.empty?
87
+ log_debug "[ Credentials for #{id} ] - Credentials not found from environment variables."
88
+ if requested_resource.nil?
89
+ log_debug "[ Credentials for #{id} ] - No resource associated to this credentials, so .netrc can't be used."
90
+ else
91
+ # Check Netrc
92
+ netrc = ::Netrc.read
93
+ begin
94
+ netrc_user, netrc_password = netrc[
95
+ begin
96
+ URI.parse(requested_resource).host.downcase
97
+ rescue URI::InvalidURIError
98
+ requested_resource
99
+ end
100
+ ]
101
+ if netrc_user.nil?
102
+ log_debug "[ Credentials for #{id} ] - No credentials retrieved from .netrc."
103
+ # TODO: Add more credentials source if needed here
104
+ log_warn "[ Credentials for #{id} ] - Unable to get credentials for #{id} (Resource: #{requested_resource})."
105
+ else
106
+ user = netrc_user.dup
107
+ password = netrc_password.dup
108
+ log_debug "[ Credentials for #{id} ] - Credentials retrieved from .netrc using #{requested_resource}."
109
+ end
110
+ ensure
111
+ # Make sure the password does not stay in Netrc memory
112
+ # Wipe out any memory trace that might contain passwords in clear
113
+ netrc.instance_variable_get(:@data).each do |data_line|
114
+ data_line.each do |data_string|
115
+ data_string.replace('GotYou!!!' * 100)
116
+ end
114
117
  end
118
+ # We do this assignment on purpose so that GC can remove sensitive data later
119
+ # rubocop:disable Lint/UselessAssignment
120
+ netrc = nil
121
+ # rubocop:enable Lint/UselessAssignment
115
122
  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
120
123
  end
124
+ else
125
+ log_debug "[ Credentials for #{id} ] - Credentials retrieved from environment variables."
121
126
  end
122
- else
123
- log_debug "[ Credentials for #{@id} ] - Credentials retrieved from environment variables."
127
+ GC.start
128
+ requester.call user, password
129
+ password&.replace('gotyou!' * 100)
130
+ GC.start
124
131
  end
125
- GC.start
132
+
133
+ requester_called = false
134
+ provider.call(
135
+ resource,
136
+ proc do |user, password|
137
+ requester_called = true
138
+ yield user, password
139
+ end
140
+ )
141
+
142
+ 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
143
  end
127
144
 
128
145
  end