kpm 0.7.2 → 0.10.0

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