kpm 0.7.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/.rubocop.yml +138 -0
  4. data/Gemfile +2 -0
  5. data/README.adoc +144 -107
  6. data/Rakefile +2 -1
  7. data/bin/kpm +4 -2
  8. data/kpm.gemspec +11 -8
  9. data/lib/kpm.rb +3 -0
  10. data/lib/kpm/account.rb +268 -338
  11. data/lib/kpm/base_artifact.rb +33 -39
  12. data/lib/kpm/base_installer.rb +69 -83
  13. data/lib/kpm/blob.rb +29 -0
  14. data/lib/kpm/cli.rb +3 -1
  15. data/lib/kpm/coordinates.rb +10 -12
  16. data/lib/kpm/database.rb +94 -113
  17. data/lib/kpm/diagnostic_file.rb +126 -147
  18. data/lib/kpm/formatter.rb +76 -48
  19. data/lib/kpm/inspector.rb +24 -34
  20. data/lib/kpm/installer.rb +53 -46
  21. data/lib/kpm/kaui_artifact.rb +4 -3
  22. data/lib/kpm/killbill_plugin_artifact.rb +10 -7
  23. data/lib/kpm/killbill_server_artifact.rb +13 -12
  24. data/lib/kpm/migrations.rb +26 -11
  25. data/lib/kpm/nexus_helper/actions.rb +52 -9
  26. data/lib/kpm/nexus_helper/cloudsmith_api_calls.rb +83 -0
  27. data/lib/kpm/nexus_helper/github_api_calls.rb +70 -0
  28. data/lib/kpm/nexus_helper/nexus_api_calls_v2.rb +130 -108
  29. data/lib/kpm/nexus_helper/nexus_facade.rb +5 -3
  30. data/lib/kpm/plugins_directory.rb +9 -8
  31. data/lib/kpm/plugins_directory.yml +14 -173
  32. data/lib/kpm/plugins_manager.rb +29 -24
  33. data/lib/kpm/sha1_checker.rb +31 -18
  34. data/lib/kpm/system.rb +104 -135
  35. data/lib/kpm/system_helpers/cpu_information.rb +56 -55
  36. data/lib/kpm/system_helpers/disk_space_information.rb +60 -63
  37. data/lib/kpm/system_helpers/entropy_available.rb +37 -39
  38. data/lib/kpm/system_helpers/memory_information.rb +52 -51
  39. data/lib/kpm/system_helpers/os_information.rb +45 -47
  40. data/lib/kpm/system_helpers/system_proxy.rb +10 -10
  41. data/lib/kpm/tasks.rb +381 -438
  42. data/lib/kpm/tenant_config.rb +68 -83
  43. data/lib/kpm/tomcat_manager.rb +10 -8
  44. data/lib/kpm/trace_logger.rb +18 -16
  45. data/lib/kpm/uninstaller.rb +81 -14
  46. data/lib/kpm/utils.rb +13 -14
  47. data/lib/kpm/version.rb +3 -1
  48. data/packaging/Gemfile +2 -0
  49. data/pom.xml +211 -40
  50. data/spec/kpm/remote/base_artifact_spec.rb +20 -20
  51. data/spec/kpm/remote/base_installer_spec.rb +35 -34
  52. data/spec/kpm/remote/cloudsmith_api_calls_spec.rb +40 -0
  53. data/spec/kpm/remote/github_api_calls_spec.rb +40 -0
  54. data/spec/kpm/remote/installer_spec.rb +80 -79
  55. data/spec/kpm/remote/kaui_artifact_spec.rb +7 -6
  56. data/spec/kpm/remote/killbill_plugin_artifact_spec.rb +25 -30
  57. data/spec/kpm/remote/killbill_server_artifact_spec.rb +17 -16
  58. data/spec/kpm/remote/migrations_spec.rb +12 -11
  59. data/spec/kpm/remote/nexus_facade_spec.rb +32 -28
  60. data/spec/kpm/remote/tenant_config_spec.rb +30 -29
  61. data/spec/kpm/remote/tomcat_manager_spec.rb +4 -3
  62. data/spec/kpm/unit/actions_spec.rb +52 -0
  63. data/spec/kpm/unit/base_artifact_spec.rb +19 -18
  64. data/spec/kpm/unit/cpu_information_spec.rb +67 -0
  65. data/spec/kpm/unit/disk_space_information_spec.rb +47 -0
  66. data/spec/kpm/unit/entropy_information_spec.rb +36 -0
  67. data/spec/kpm/unit/formatter_spec.rb +163 -0
  68. data/spec/kpm/unit/inspector_spec.rb +34 -42
  69. data/spec/kpm/unit/installer_spec.rb +7 -6
  70. data/spec/kpm/unit/memory_information_spec.rb +102 -0
  71. data/spec/kpm/unit/os_information_spec.rb +38 -0
  72. data/spec/kpm/unit/plugins_directory_spec.rb +38 -22
  73. data/spec/kpm/unit/plugins_manager_spec.rb +62 -66
  74. data/spec/kpm/unit/sha1_checker_spec.rb +107 -60
  75. data/spec/kpm/unit/uninstaller_spec.rb +118 -72
  76. data/spec/kpm/unit_mysql/account_spec.rb +127 -142
  77. data/spec/spec_helper.rb +20 -18
  78. data/tasks/package.rake +18 -18
  79. metadata +42 -22
@@ -1,12 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rexml/document'
2
4
  require 'set'
3
5
 
4
6
  module KPM
5
7
  class KauiArtifact < BaseArtifact
6
8
  class << self
7
- def versions(overrides={}, ssl_verify=true)
8
-
9
- coordinate_map = {:group_id => KPM::BaseArtifact::KAUI_GROUP_ID, :artifact_id => KPM::BaseArtifact::KAUI_ARTIFACT_ID, :packaging => KPM::BaseArtifact::KAUI_PACKAGING, :classifier => KPM::BaseArtifact::KAUI_CLASSIFIER}
9
+ def versions(overrides = {}, ssl_verify = true)
10
+ coordinate_map = { group_id: KPM::BaseArtifact::KAUI_GROUP_ID, artifact_id: KPM::BaseArtifact::KAUI_ARTIFACT_ID, packaging: KPM::BaseArtifact::KAUI_PACKAGING, classifier: KPM::BaseArtifact::KAUI_CLASSIFIER }
10
11
 
11
12
  coordinates = KPM::Coordinates.build_coordinates(coordinate_map)
12
13
  response = REXML::Document.new nexus_remote(overrides, ssl_verify).search_for_artifacts(coordinates)
@@ -1,23 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rexml/document'
2
4
  require 'set'
3
5
 
4
6
  module KPM
5
7
  class KillbillPluginArtifact < BaseArtifact
6
8
  class << self
7
- def pull(logger, group_id, artifact_id, packaging='jar', classifier=nil, version='LATEST', plugin_name=nil, destination_path=nil, sha1_file=nil, force_download=false, verify_sha1=true, overrides={}, ssl_verify=true)
8
- coordinate_map = {:group_id => group_id, :artifact_id => artifact_id, :packaging => packaging, :classifier => classifier, :version => version}
9
- pull_and_put_in_place(logger, coordinate_map, plugin_name, destination_path, is_ruby_plugin_and_should_skip_top_dir(group_id, artifact_id), sha1_file, force_download, verify_sha1, overrides, ssl_verify)
9
+ def pull(logger, group_id, artifact_id, packaging = 'jar', classifier = nil, version = 'LATEST', plugin_name = nil, destination_path = nil, sha1_file = nil, force_download = false, verify_sha1 = true, overrides = {}, ssl_verify = true)
10
+ coordinate_map = { group_id: group_id, artifact_id: artifact_id, packaging: packaging, classifier: classifier, version: version }
11
+ pull_and_put_in_place(logger, coordinate_map, plugin_name, destination_path, ruby_plugin_and_should_skip_top_dir?(group_id, artifact_id), sha1_file, force_download, verify_sha1, overrides, ssl_verify)
10
12
  end
11
13
 
12
- def versions(overrides={}, ssl_verify=true)
13
- plugins = {:java => {}, :ruby => {}}
14
+ def versions(overrides = {}, ssl_verify = true)
15
+ plugins = { java: {}, ruby: {} }
14
16
 
15
17
  nexus = nexus_remote(overrides, ssl_verify)
16
18
 
17
19
  [[:java, KPM::BaseArtifact::KILLBILL_JAVA_PLUGIN_GROUP_ID], [:ruby, KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_GROUP_ID]].each do |type_and_group_id|
18
20
  response = REXML::Document.new nexus.search_for_artifacts(type_and_group_id[1])
19
21
  response.elements.each('searchNGResponse/data/artifact') do |element|
20
- artifact_id = element.elements['artifactId'].text
22
+ artifact_id = element.elements['artifactId'].text
21
23
  plugins[type_and_group_id[0]][artifact_id] ||= SortedSet.new
22
24
  plugins[type_and_group_id[0]][artifact_id] << element.elements['version'].text
23
25
  end
@@ -27,9 +29,10 @@ module KPM
27
29
  end
28
30
 
29
31
  protected
32
+
30
33
  # Magic methods...
31
34
 
32
- def is_ruby_plugin_and_should_skip_top_dir(group_id, artifact_id)
35
+ def ruby_plugin_and_should_skip_top_dir?(group_id, artifact_id)
33
36
  # The second check is for custom ruby plugins
34
37
  group_id == KPM::BaseArtifact::KILLBILL_RUBY_PLUGIN_GROUP_ID || artifact_id.include?('plugin')
35
38
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'rexml/document'
2
4
  require 'set'
3
5
 
4
6
  module KPM
5
7
  class KillbillServerArtifact < BaseArtifact
6
8
  class << self
7
- def versions(artifact_id, packaging=KPM::BaseArtifact::KILLBILL_PACKAGING, classifier=KPM::BaseArtifact::KILLBILL_CLASSIFIER, overrides={}, ssl_verify=true)
8
- coordinate_map = {:group_id => KPM::BaseArtifact::KILLBILL_GROUP_ID, :artifact_id => artifact_id, :packaging => packaging, :classifier => classifier}
9
+ def versions(artifact_id, packaging = KPM::BaseArtifact::KILLBILL_PACKAGING, classifier = KPM::BaseArtifact::KILLBILL_CLASSIFIER, overrides = {}, ssl_verify = true)
10
+ coordinate_map = { group_id: KPM::BaseArtifact::KILLBILL_GROUP_ID, artifact_id: artifact_id, packaging: packaging, classifier: classifier }
9
11
  coordinates = KPM::Coordinates.build_coordinates(coordinate_map)
10
12
  response = REXML::Document.new nexus_remote(overrides, ssl_verify).search_for_artifacts(coordinates)
11
13
  versions = SortedSet.new
@@ -13,14 +15,15 @@ module KPM
13
15
  versions
14
16
  end
15
17
 
16
- def info(version='LATEST', sha1_file=nil, force_download=false, verify_sha1=true, overrides={}, ssl_verify=true)
18
+ def info(version = 'LATEST', sha1_file = nil, force_download = false, verify_sha1 = true, overrides = {}, ssl_verify = true)
17
19
  logger = Logger.new(STDOUT)
18
20
  logger.level = Logger::ERROR
19
21
 
20
- version = KPM::Installer.get_kb_latest_stable_version if version == 'LATEST'
21
-
22
+ # Initialize as early as possible (used in rescue block below)
22
23
  sha1_checker = sha1_file ? Sha1Checker.from_file(sha1_file) : nil
23
24
 
25
+ version = KPM::Installer.get_kb_latest_stable_version if version == 'LATEST'
26
+
24
27
  versions = {}
25
28
  Dir.mktmpdir do |dir|
26
29
  # Retrieve the main Kill Bill pom
@@ -61,7 +64,7 @@ module KPM
61
64
 
62
65
  pom = REXML::Document.new(File.new(oss_pom_info[:file_path]))
63
66
  properties_element = pom.root.elements['properties']
64
- %w(killbill-api killbill-plugin-api killbill-commons killbill-platform).each do |property|
67
+ %w[killbill-api killbill-plugin-api killbill-commons killbill-platform].each do |property|
65
68
  versions[property] = properties_element.elements["#{property}.version"].text
66
69
  end
67
70
 
@@ -71,12 +74,10 @@ module KPM
71
74
  rescue StandardError => e
72
75
  # Network down? Hopefully, we have something in the cache
73
76
  cached_version = sha1_checker ? sha1_checker.killbill_info(version) : nil
74
- if force_download || !cached_version
75
- raise e
76
- else
77
- # Use the cache
78
- return cached_version
79
- end
77
+ raise e if force_download || !cached_version
78
+
79
+ # Use the cache
80
+ cached_version
80
81
  end
81
82
  end
82
83
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
  require 'json'
3
5
  require 'logger'
@@ -6,10 +8,9 @@ require 'pathname'
6
8
 
7
9
  module KPM
8
10
  class Migrations
9
-
10
- KILLBILL_MIGRATION_PATH = /src\/main\/resources\/org\/killbill\/billing\/[a-z]+\/migration\/(V[0-9a-zA-Z_]+.sql)/
11
- JAVA_PLUGIN_MIGRATION_PATH = /src\/main\/resources\/migration\/(V[0-9a-zA-Z_]+.sql)/
12
- RUBY_PLUGIN_MIGRATION_PATH = /db\/migrate\/([0-9a-zA-Z_]+.rb)/
11
+ KILLBILL_MIGRATION_PATH = %r{src/main/resources/org/killbill/billing/[a-z]+/migration/(V[0-9a-zA-Z_]+.sql)}.freeze
12
+ JAVA_PLUGIN_MIGRATION_PATH = %r{src/main/resources/migration/(V[0-9a-zA-Z_]+.sql)}.freeze
13
+ RUBY_PLUGIN_MIGRATION_PATH = %r{db/migrate/([0-9a-zA-Z_]+.rb)}.freeze
13
14
 
14
15
  # Go to https://github.com/settings/tokens to generate a token
15
16
  def initialize(from_version, to_version = nil, repository = 'killbill/killbill', oauth_token = nil, logger = Logger.new(STDOUT))
@@ -34,7 +35,7 @@ module KPM
34
35
  end
35
36
 
36
37
  def save(dir = nil)
37
- return nil if migrations.size == 0
38
+ return nil if migrations.empty?
38
39
 
39
40
  dir ||= Dir.mktmpdir
40
41
  @logger.debug("Storing migrations to #{dir}")
@@ -53,7 +54,7 @@ module KPM
53
54
 
54
55
  def for_version(version = @from_version, name_only = false, migrations_to_skip = Set.new)
55
56
  @logger.info("Looking for migrations repository=#{@repository}, version=#{version}")
56
- metadata = get_as_json("https://api.github.com/repos/#{@repository}/git/trees/#{version}?recursive=1&access_token=#{@oauth_token}")
57
+ metadata = get_as_json("https://api.github.com/repos/#{@repository}/git/trees/#{version}?recursive=1")
57
58
 
58
59
  migrations = []
59
60
  metadata['tree'].each do |entry|
@@ -66,13 +67,13 @@ module KPM
66
67
 
67
68
  sql = nil
68
69
  unless name_only
69
- blob_metadata = get_as_json("#{entry['url']}?access_token=#{@oauth_token}")
70
+ blob_metadata = get_as_json((entry['url']).to_s)
70
71
  sql = decode(blob_metadata['content'], blob_metadata['encoding'])
71
72
  end
72
73
 
73
74
  migrations << {
74
- :name => migration_name,
75
- :sql => sql
75
+ name: migration_name,
76
+ sql: sql
76
77
  }
77
78
  end
78
79
 
@@ -80,8 +81,22 @@ module KPM
80
81
  end
81
82
 
82
83
  def get_as_json(url)
83
- raw = URI.parse(url).read
84
- JSON.parse(raw)
84
+ uri = URI.parse(url)
85
+ http = Net::HTTP.new(uri.host, uri.port)
86
+ http.use_ssl = uri.scheme == 'https'
87
+
88
+ path = uri.path || '/'
89
+ path = "#{path}?#{uri.query}" unless uri.query.nil?
90
+ request = Net::HTTP::Get.new(path)
91
+ request['Authorization'] = "token #{@oauth_token}" unless @oauth_token.nil?
92
+
93
+ response = http.request(request)
94
+ case response.code
95
+ when '200'
96
+ JSON.parse(response.body)
97
+ else
98
+ raise "Unable to download #{url}: #{response.code}"
99
+ end
85
100
  end
86
101
 
87
102
  def decode(content, encoding)
@@ -1,9 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'nexus_api_calls_v2'
2
- #require_relative 'nexus_api_calls_v3'
4
+ require_relative 'github_api_calls'
5
+ require_relative 'cloudsmith_api_calls'
3
6
 
4
7
  module KPM
5
8
  module NexusFacade
6
9
  class Actions
10
+ DEFAULT_RETRIES = 3
11
+ DEFAULT_CONNECTION_ERRORS = {
12
+ EOFError => 'The remote server dropped the connection',
13
+ Errno::ECONNREFUSED => 'The remote server refused the connection',
14
+ Errno::ECONNRESET => 'The remote server reset the connection',
15
+ Timeout::Error => 'The connection to the remote server timed out',
16
+ Errno::ETIMEDOUT => 'The connection to the remote server timed out',
17
+ SocketError => 'The connection to the remote server could not be established',
18
+ OpenSSL::X509::CertificateError => 'The remote server did not accept the provided SSL certificate',
19
+ OpenSSL::SSL::SSLError => 'The SSL connection to the remote server could not be established',
20
+ Zlib::BufError => 'The remote server replied with an invalid response',
21
+ KPM::NexusFacade::UnexpectedStatusCodeException => nil
22
+ }.freeze
23
+
7
24
  attr_reader :nexus_api_call
8
25
 
9
26
  def initialize(overrides, ssl_verify, logger)
@@ -11,24 +28,50 @@ module KPM
11
28
  overrides[:url] ||= 'https://oss.sonatype.org'
12
29
  overrides[:repository] ||= 'releases'
13
30
 
14
- #this is where the version is verified
15
- #example if
16
- #@nexus_api_call = overrides['version'] == '3' ? NexusApiCallsV3.new(overrides, ssl_verify) : NexusApiCallsV2.new(overrides, ssl_verify)
17
- @nexus_api_call = NexusApiCallsV2.new(overrides, ssl_verify, logger)
31
+ @logger = logger
32
+
33
+ @nexus_api_call = if overrides[:url].start_with?('https://maven.pkg.github.com')
34
+ GithubApiCalls.new(overrides, ssl_verify, logger)
35
+ elsif overrides[:url].start_with?('https://dl.cloudsmith.io')
36
+ CloudsmithApiCalls.new(overrides, ssl_verify, logger)
37
+ else
38
+ NexusApiCallsV2.new(overrides, ssl_verify, logger)
39
+ end
18
40
  end
19
41
 
20
- def pull_artifact(coordinates, destination=nil)
21
- nexus_api_call.pull_artifact(coordinates, destination)
42
+ def pull_artifact(coordinates, destination = nil)
43
+ retry_exceptions("pull_artifact #{coordinates}") { nexus_api_call.pull_artifact(coordinates, destination) }
22
44
  end
23
45
 
24
46
  def get_artifact_info(coordinates)
25
- nexus_api_call.get_artifact_info(coordinates)
47
+ retry_exceptions("get_artifact_info #{coordinates}") { nexus_api_call.get_artifact_info(coordinates) }
26
48
  end
27
49
 
28
50
  def search_for_artifacts(coordinates)
29
- nexus_api_call.search_for_artifacts(coordinates)
51
+ retry_exceptions("search_for_artifacts #{coordinates}") { nexus_api_call.search_for_artifacts(coordinates) }
52
+ end
53
+
54
+ private
55
+
56
+ def retry_exceptions(tag)
57
+ retries = DEFAULT_RETRIES
58
+
59
+ begin
60
+ yield
61
+ rescue *DEFAULT_CONNECTION_ERRORS.keys => e
62
+ retries -= 1
63
+
64
+ @logger.warn(format('Transient error during %<tag>s, retrying (attempt=%<attempt>d): %<msg>s', tag: tag, attempt: DEFAULT_RETRIES - retries, msg: derived_error_message(DEFAULT_CONNECTION_ERRORS, e)))
65
+ retry unless retries.zero?
66
+
67
+ raise
68
+ end
30
69
  end
31
70
 
71
+ def derived_error_message(errors, exception)
72
+ key = (errors.keys & exception.class.ancestors).first
73
+ (key ? errors[key] : nil) || exception.message
74
+ end
32
75
  end
33
76
  end
34
77
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'rexml/document'
6
+ require 'openssl'
7
+
8
+ module KPM
9
+ module NexusFacade
10
+ class CloudsmithApiCalls < NexusApiCallsV2
11
+ def pull_artifact_endpoint(coordinates)
12
+ version_artifact_details = parent_get_artifact_info(coordinates)
13
+
14
+ # For SNAPSHOTs, we need to figure out the version used as part of the filename
15
+ filename_version = begin
16
+ REXML::Document.new(version_artifact_details).elements['//versioning/snapshotVersions/snapshotVersion[1]/value'].text
17
+ rescue StandardError
18
+ nil
19
+ end
20
+ coords = parse_coordinates(coordinates)
21
+ coords[:version] = filename_version unless filename_version.nil?
22
+ new_coordinates = coords.values.compact.join(':')
23
+
24
+ base_path, versioned_artifact, = build_base_path_and_coords(new_coordinates)
25
+ "#{base_path}/#{versioned_artifact}"
26
+ end
27
+
28
+ alias parent_get_artifact_info get_artifact_info
29
+ def get_artifact_info(coordinates)
30
+ super
31
+
32
+ _, versioned_artifact, coords = build_base_path_and_coords(coordinates)
33
+ sha1 = get_sha1(coordinates)
34
+ "<artifact-resolution>
35
+ <data>
36
+ <presentLocally>true</presentLocally>
37
+ <groupId>#{coords[:group_id]}</groupId>
38
+ <artifactId>#{coords[:artifact_id]}</artifactId>
39
+ <version>#{coords[:version]}</version>
40
+ <extension>#{coords[:packaging]}</extension>
41
+ <snapshot>#{!(coords[:version] =~ /-SNAPSHOT$/).nil?}</snapshot>
42
+ <sha1>#{sha1}</sha1>
43
+ <repositoryPath>/#{coords[:group_id].gsub('.', '/')}/#{versioned_artifact}</repositoryPath>
44
+ </data>
45
+ </artifact-resolution>"
46
+ end
47
+
48
+ def get_artifact_info_endpoint(coordinates)
49
+ base_path, _, coords = build_base_path_and_coords(coordinates)
50
+ # Note: we must retrieve the XML for the version, to support SNAPSHOTs
51
+ "#{base_path}/#{coords[:version]}/maven-metadata.xml"
52
+ end
53
+
54
+ def search_for_artifact_endpoint(_coordinates)
55
+ raise NoMethodError, 'Cloudsmith has no search support'
56
+ end
57
+
58
+ def build_query_params(_coordinates, _what_parameters = nil)
59
+ ''
60
+ end
61
+
62
+ private
63
+
64
+ def get_sha1(coordinates)
65
+ base_path, versioned_artifact, = build_base_path_and_coords(coordinates)
66
+ endpoint = "#{base_path}/#{versioned_artifact}.sha1"
67
+ get_response_with_retries(coordinates, endpoint, nil)
68
+ end
69
+
70
+ def build_base_path_and_coords(coordinates)
71
+ coords = parse_coordinates(coordinates)
72
+
73
+ token_org_and_repo = URI.parse(configuration[:url]).path
74
+
75
+ [
76
+ "#{token_org_and_repo}/#{coords[:group_id].gsub('.', '/')}/#{coords[:artifact_id]}",
77
+ "#{coords[:version]}/#{coords[:artifact_id]}-#{coords[:version]}.#{coords[:extension]}",
78
+ coords
79
+ ]
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'rexml/document'
6
+ require 'openssl'
7
+
8
+ module KPM
9
+ module NexusFacade
10
+ class GithubApiCalls < NexusApiCallsV2
11
+ def pull_artifact_endpoint(coordinates)
12
+ base_path, versioned_artifact, = build_base_path_and_coords(coordinates)
13
+ "#{base_path}/#{versioned_artifact}"
14
+ end
15
+
16
+ def get_artifact_info(coordinates)
17
+ super
18
+
19
+ _, versioned_artifact, coords = build_base_path_and_coords(coordinates)
20
+ sha1 = get_sha1(coordinates)
21
+ "<artifact-resolution>
22
+ <data>
23
+ <presentLocally>true</presentLocally>
24
+ <groupId>#{coords[:group_id]}</groupId>
25
+ <artifactId>#{coords[:artifact_id]}</artifactId>
26
+ <version>#{coords[:version]}</version>
27
+ <extension>#{coords[:packaging]}</extension>
28
+ <snapshot>#{!(coords[:version] =~ /-SNAPSHOT$/).nil?}</snapshot>
29
+ <sha1>#{sha1}</sha1>
30
+ <repositoryPath>/#{coords[:group_id].gsub('.', '/')}/#{versioned_artifact}</repositoryPath>
31
+ </data>
32
+ </artifact-resolution>"
33
+ end
34
+
35
+ def get_artifact_info_endpoint(coordinates)
36
+ base_path, = build_base_path_and_coords(coordinates)
37
+ "#{base_path}/maven-metadata.xml"
38
+ end
39
+
40
+ def search_for_artifact_endpoint(_coordinates)
41
+ raise NoMethodError, 'GitHub Packages has no search support'
42
+ end
43
+
44
+ def build_query_params(_coordinates, _what_parameters = nil)
45
+ ''
46
+ end
47
+
48
+ private
49
+
50
+ def get_sha1(coordinates)
51
+ base_path, versioned_artifact, = build_base_path_and_coords(coordinates)
52
+ endpoint = "#{base_path}/#{versioned_artifact}.sha1"
53
+ get_response_with_retries(coordinates, endpoint, nil)
54
+ end
55
+
56
+ def build_base_path_and_coords(coordinates)
57
+ coords = parse_coordinates(coordinates)
58
+
59
+ # The url may contain the org and repo, e.g. 'https://maven.pkg.github.com/killbill/qualpay-java-client'
60
+ org_and_repo = URI.parse(configuration[:url]).path
61
+
62
+ [
63
+ "#{org_and_repo}/#{coords[:group_id].gsub('.', '/')}/#{coords[:artifact_id]}",
64
+ "#{coords[:version]}/#{coords[:artifact_id]}-#{coords[:version]}.#{coords[:extension]}",
65
+ coords
66
+ ]
67
+ end
68
+ end
69
+ end
70
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'net/http'
2
4
  require 'uri'
3
5
  require 'rexml/document'
@@ -5,13 +7,12 @@ require 'openssl'
5
7
 
6
8
  module KPM
7
9
  module NexusFacade
8
-
9
10
  class UnexpectedStatusCodeException < StandardError
10
11
  def initialize(code)
11
12
  @code = code
12
13
  end
13
14
 
14
- def message
15
+ def message
15
16
  "The server responded with a #{@code} status code which is unexpected."
16
17
  end
17
18
  end
@@ -26,19 +27,13 @@ module KPM
26
27
 
27
28
  # This is an extract and slim down of functions needed from nexus_cli to maintain the response expected by the base_artifact.
28
29
  class NexusApiCallsV2
29
- PULL_ARTIFACT_ENDPOINT = '/service/local/artifact/maven/redirect'
30
- GET_ARTIFACT_INFO_ENDPOINT = '/service/local/artifact/maven/resolve'
31
- SEARCH_FOR_ARTIFACT_ENDPOINT = '/service/local/lucene/search'
32
-
33
30
  READ_TIMEOUT_DEFAULT = 60
34
31
  OPEN_TIMEOUT_DEFAULT = 60
35
32
 
36
33
  ERROR_MESSAGE_404 = 'The artifact you requested information for could not be found. Please ensure it exists inside the Nexus.'
37
- ERROR_MESSAGE_503 = 'Could not connect to Nexus. Please ensure the url you are using is reachable.'
38
34
 
39
- attr_reader :version
40
- attr_reader :configuration
41
- attr_reader :ssl_verify
35
+ attr_reader :version, :configuration, :ssl_verify
36
+
42
37
  attr_accessor :logger
43
38
 
44
39
  def initialize(configuration, ssl_verify, logger)
@@ -49,141 +44,168 @@ module KPM
49
44
 
50
45
  def search_for_artifacts(coordinates)
51
46
  logger.debug "Entered - Search for artifact, coordinates: #{coordinates}"
52
- response = get_response(coordinates, SEARCH_FOR_ARTIFACT_ENDPOINT, [:g, :a])
47
+ response = get_response(coordinates, search_for_artifact_endpoint(coordinates), %i[g a])
53
48
 
54
49
  case response.code
55
- when '200'
56
- logger.debug "response body: #{response.body}"
57
- return response.body
58
- else
59
- raise UnexpectedStatusCodeException.new(response.code)
50
+ when '200'
51
+ logger.debug "response body: #{response.body}"
52
+ response.body
53
+ when '404'
54
+ raise StandardError, ERROR_MESSAGE_404
55
+ else
56
+ raise UnexpectedStatusCodeException, response.code
60
57
  end
61
58
  end
62
59
 
63
60
  def get_artifact_info(coordinates)
64
- logger.debug "Entered - Get artifact info, coordinates: #{coordinates}"
65
- response = get_response(coordinates, GET_ARTIFACT_INFO_ENDPOINT, nil)
66
-
67
- case response.code
68
- when '200'
69
- logger.debug "response body: #{response.body}"
70
- return response.body
71
- when '404'
72
- raise StandardError.new(ERROR_MESSAGE_404)
73
- when '503'
74
- raise StandardError.new(ERROR_MESSAGE_503)
75
- else
76
- raise UnexpectedStatusCodeException.new(response.code)
77
- end
61
+ get_response_with_retries(coordinates, get_artifact_info_endpoint(coordinates), nil)
78
62
  end
79
63
 
80
- def pull_artifact(coordinates ,destination)
81
- logger.debug "Entered - Pull artifact, coordinates: #{coordinates}"
64
+ def pull_artifact(coordinates, destination)
82
65
  file_name = get_file_name(coordinates)
83
- destination = File.join(File.expand_path(destination || "."), file_name)
84
- logger.debug "destination: #{destination}"
85
- response = get_response(coordinates, PULL_ARTIFACT_ENDPOINT, nil)
66
+ destination = File.join(File.expand_path(destination || '.'), file_name)
67
+ logger.debug { "Downloading to destination: #{destination}" }
86
68
 
87
- case response.code
88
- when '301', '307'
89
- location = response['Location'].gsub!(configuration[:url],'')
90
- logger.debug 'fetching artifact'
91
- file_response = get_response(nil,location, nil)
92
-
93
- File.open(destination, "wb") do |io|
94
- io.write(file_response.body)
95
- end
96
- when 404
97
- raise StandardError.new(ERROR_MESSAGE_404)
98
- else
99
- raise UnexpectedStatusCodeException.new(response.code)
69
+ File.open(destination, 'wb') do |io|
70
+ io.write(get_response_with_retries(coordinates, pull_artifact_endpoint(coordinates), nil))
100
71
  end
72
+
101
73
  {
102
- :file_name => file_name,
103
- :file_path => File.expand_path(destination),
104
- :version => version,
105
- :size => File.size(File.expand_path(destination))
74
+ file_name: file_name,
75
+ file_path: File.expand_path(destination),
76
+ version: version,
77
+ size: File.size(File.expand_path(destination))
106
78
  }
107
79
  end
108
80
 
109
- private
110
- def parse_coordinates(coordinates)
81
+ def pull_artifact_endpoint(_coordinates)
82
+ '/service/local/artifact/maven/redirect'
83
+ end
111
84
 
112
- if coordinates.nil?
113
- raise ArtifactMalformedException
114
- end
85
+ def get_artifact_info_endpoint(_coordinates)
86
+ '/service/local/artifact/maven/resolve'
87
+ end
115
88
 
116
- split_coordinates = coordinates.split(":")
117
- if (split_coordinates.size == 0 or split_coordinates.size > 5)
118
- raise ArtifactMalformedException
119
- end
89
+ def search_for_artifact_endpoint(_coordinates)
90
+ '/service/local/lucene/search'
91
+ end
120
92
 
121
- artifact = Hash.new
93
+ def build_query_params(coordinates, what_parameters = nil)
94
+ artifact = parse_coordinates(coordinates)
95
+ @version = artifact[:version].to_s.upcase
122
96
 
123
- artifact[:group_id] = split_coordinates[0]
124
- artifact[:artifact_id] = split_coordinates[1]
125
- artifact[:extension] = split_coordinates.size > 3 ? split_coordinates[2] : "jar"
126
- artifact[:classifier] = split_coordinates.size > 4 ? split_coordinates[3] : nil
127
- artifact[:version] = split_coordinates[-1]
97
+ query = { g: artifact[:group_id], a: artifact[:artifact_id], e: artifact[:extension], v: version, r: configuration[:repository] }
98
+ query.merge!(c: artifact[:classifier]) unless artifact[:classifier].nil?
128
99
 
129
- artifact[:version].upcase! if version == "latest"
100
+ params = what_parameters.nil? ? query : {}
101
+ what_parameters.each { |key| params[key] = query[key] unless query[key].nil? } unless what_parameters.nil?
130
102
 
131
- return artifact
132
- end
103
+ params.map { |key, value| "#{key}=#{value}" }.join('&')
104
+ end
133
105
 
134
- def get_file_name(coordinates)
135
- artifact = parse_coordinates(coordinates)
106
+ private
136
107
 
137
- if artifact[:version].casecmp("latest")
138
- artifact[:version] = REXML::Document.new(get_artifact_info(coordinates)).elements["//version"].text
139
- end
108
+ def parse_coordinates(coordinates)
109
+ raise ArtifactMalformedException if coordinates.nil?
140
110
 
141
- if artifact[:classifier].nil?
142
- "#{artifact[:artifact_id]}-#{artifact[:version]}.#{artifact[:extension]}"
143
- else
144
- "#{artifact[:artifact_id]}-#{artifact[:version]}-#{artifact[:classifier]}.#{artifact[:extension]}"
145
- end
146
- end
111
+ split_coordinates = coordinates.split(':')
112
+ raise ArtifactMalformedException if split_coordinates.empty? || (split_coordinates.size > 5)
147
113
 
148
- def get_query_params(coordinates, what_parameters = nil)
149
- artifact = parse_coordinates(coordinates)
150
- @version = artifact[:version].to_s.upcase
114
+ artifact = {}
151
115
 
152
- query = {:g => artifact[:group_id], :a => artifact[:artifact_id], :e => artifact[:extension], :v => version, :r => configuration[:repository]}
153
- query.merge!({:c => artifact[:classifier]}) unless artifact[:classifier].nil?
116
+ artifact[:group_id] = split_coordinates[0]
117
+ artifact[:artifact_id] = split_coordinates[1]
118
+ artifact[:extension] = split_coordinates.size > 3 ? split_coordinates[2] : 'jar'
119
+ artifact[:classifier] = split_coordinates.size > 4 ? split_coordinates[3] : nil
120
+ artifact[:version] = split_coordinates[-1]
154
121
 
155
- params = what_parameters.nil? ? query : Hash.new
156
- what_parameters.each {|key| params[key] = query[key] unless query[key].nil? } unless what_parameters.nil?
122
+ artifact[:version].upcase! if version == 'latest'
157
123
 
158
- params.map{|key,value| "#{key}=#{value}"}.join('&')
159
- end
124
+ artifact
125
+ end
160
126
 
161
- def get_response(coordinates, endpoint, what_parameters)
162
- http = get_http
163
- query_params = get_query_params(coordinates, what_parameters) unless coordinates.nil?
164
- endpoint = get_endpoint_with_params(endpoint, query_params) unless coordinates.nil?
165
- request = Net::HTTP::Get.new(endpoint)
127
+ def get_file_name(coordinates)
128
+ artifact = parse_coordinates(coordinates)
166
129
 
167
- logger.debug "request endpoint: #{endpoint}"
130
+ artifact[:version] = REXML::Document.new(get_artifact_info(coordinates)).elements['//version'].text if artifact[:version].casecmp('latest')
168
131
 
169
- response = http.request(request)
170
- response
132
+ if artifact[:classifier].nil?
133
+ "#{artifact[:artifact_id]}-#{artifact[:version]}.#{artifact[:extension]}"
134
+ else
135
+ "#{artifact[:artifact_id]}-#{artifact[:version]}-#{artifact[:classifier]}.#{artifact[:extension]}"
136
+ end
137
+ end
138
+
139
+ def get_response_with_retries(coordinates, endpoint, what_parameters)
140
+ logger.debug { "Fetching coordinates=#{coordinates}, endpoint=#{endpoint}, params=#{what_parameters}" }
141
+ response = get_response(coordinates, endpoint, what_parameters)
142
+ logger.debug { "Response body: #{response.body}" }
143
+ process_response_with_retries(response)
144
+ end
145
+
146
+ def process_response_with_retries(response)
147
+ case response.code
148
+ when '200'
149
+ response.body
150
+ when '301', '307'
151
+ location = response['Location']
152
+ logger.debug { "Following redirect to #{location}" }
153
+
154
+ new_path = location.gsub!(configuration[:url], '')
155
+ if new_path.nil?
156
+ # Redirect to another domain (e.g. CDN)
157
+ get_raw_response_with_retries(location)
158
+ else
159
+ get_response_with_retries(nil, location, nil)
160
+ end
161
+ when '404'
162
+ raise StandardError, ERROR_MESSAGE_404
163
+ else
164
+ raise UnexpectedStatusCodeException, response.code
171
165
  end
166
+ end
172
167
 
173
- def get_http
174
- uri = URI.parse(configuration[:url])
175
- http = Net::HTTP.new(uri.host,uri.port)
176
- http.open_timeout = configuration[:open_timeout] || OPEN_TIMEOUT_DEFAULT #seconds
177
- http.read_timeout = configuration[:read_timeout] || READ_TIMEOUT_DEFAULT #seconds
178
- http.use_ssl = (ssl_verify != false)
179
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless ssl_verify
180
- http
168
+ def get_response(coordinates, endpoint, what_parameters)
169
+ http = build_http
170
+ query_params = build_query_params(coordinates, what_parameters) unless coordinates.nil?
171
+ endpoint = endpoint_with_params(endpoint, query_params) unless coordinates.nil?
172
+ request = Net::HTTP::Get.new(endpoint)
173
+ if configuration.key?(:username) && configuration.key?(:password)
174
+ request.basic_auth(configuration[:username], configuration[:password])
175
+ elsif configuration.key?(:token)
176
+ request['Authorization'] = "token #{configuration[:token]}"
181
177
  end
182
178
 
183
- def get_endpoint_with_params(endpoint,query_params)
184
- "#{endpoint}?#{URI::DEFAULT_PARSER.escape(query_params)}"
179
+ logger.debug do
180
+ http.set_debug_output(logger)
181
+ "HTTP path: #{endpoint}"
185
182
  end
186
183
 
184
+ http.request(request)
185
+ end
186
+
187
+ def build_http
188
+ uri = URI.parse(configuration[:url])
189
+ http = Net::HTTP.new(uri.host, uri.port)
190
+ http.open_timeout = configuration[:open_timeout] || OPEN_TIMEOUT_DEFAULT # seconds
191
+ http.read_timeout = configuration[:read_timeout] || READ_TIMEOUT_DEFAULT # seconds
192
+ http.use_ssl = (ssl_verify != false)
193
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE unless ssl_verify
194
+
195
+ logger.debug { "HTTP connection: #{http.inspect}" }
196
+
197
+ http
198
+ end
199
+
200
+ def get_raw_response_with_retries(location)
201
+ response = Net::HTTP.get_response(URI(location))
202
+ logger.debug { "Response body: #{response.body}" }
203
+ process_response_with_retries(response)
204
+ end
205
+
206
+ def endpoint_with_params(endpoint, query_params)
207
+ "#{endpoint}?#{URI::DEFAULT_PARSER.escape(query_params)}"
208
+ end
187
209
  end
188
210
  end
189
211
  end