kpm 0.11.2 → 0.12.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.
- checksums.yaml +4 -4
- data/lib/kpm/base_artifact.rb +1 -1
- data/lib/kpm/nexus_helper/actions.rb +5 -1
- data/lib/kpm/nexus_helper/maven_central_api_calls.rb +246 -0
- data/lib/kpm/tasks.rb +1 -1
- data/lib/kpm/version.rb +1 -1
- data/pom.xml +1 -1
- data/spec/kpm/remote/killbill_plugin_artifact_spec.rb +0 -3
- data/spec/kpm/remote/maven_central_api_calls_spec.rb +69 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb012bf11b9ab7cc9417cec007ff6ce492b73e83637c5b55a0261c8b9ef2eb7c
|
4
|
+
data.tar.gz: 88d8bad35078a098f4a71ca95bb51ec48cbafaf6923bdaf0d2a2d5853aa3b023
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 73e89976ba802f662a70235665b32a7758a5d1cb4b7a5c07344f24647d8576bafc19ffbd695c26d798ab083c3c548912bf5e9394a66c77144bed82007d9be9cf
|
7
|
+
data.tar.gz: ef42456adfdf5e953757138fa116acde1ffd8802edb098ecf87eeffcde817daebac66f34f78a00cfaad7a15d206aa84a4f36978ee5c7b1d1995af844b72e25cc
|
data/lib/kpm/base_artifact.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
3
3
|
require_relative 'nexus_api_calls_v2'
|
4
4
|
require_relative 'github_api_calls'
|
5
5
|
require_relative 'cloudsmith_api_calls'
|
6
|
+
require_relative 'maven_central_api_calls'
|
6
7
|
|
7
8
|
module KPM
|
8
9
|
module NexusFacade
|
@@ -25,15 +26,18 @@ module KPM
|
|
25
26
|
|
26
27
|
def initialize(overrides, ssl_verify, logger)
|
27
28
|
overrides ||= {}
|
28
|
-
overrides[:url] ||= 'https://
|
29
|
+
overrides[:url] ||= 'https://repo1.maven.org/maven2'
|
29
30
|
overrides[:repository] ||= 'releases'
|
30
31
|
|
31
32
|
@logger = logger
|
33
|
+
logger.level = Logger::INFO
|
32
34
|
|
33
35
|
@nexus_api_call = if overrides[:url].start_with?('https://maven.pkg.github.com')
|
34
36
|
GithubApiCalls.new(overrides, ssl_verify, logger)
|
35
37
|
elsif overrides[:url].start_with?('https://dl.cloudsmith.io')
|
36
38
|
CloudsmithApiCalls.new(overrides, ssl_verify, logger)
|
39
|
+
elsif overrides[:url].start_with?('https://repo1.maven.org')
|
40
|
+
MavenCentralApiCalls.new(overrides, ssl_verify, logger)
|
37
41
|
else
|
38
42
|
NexusApiCallsV2.new(overrides, ssl_verify, logger)
|
39
43
|
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'json'
|
6
|
+
require 'rexml/document'
|
7
|
+
|
8
|
+
module KPM
|
9
|
+
module NexusFacade
|
10
|
+
class MavenCentralApiCalls < NexusApiCallsV2
|
11
|
+
READ_TIMEOUT_DEFAULT = 60
|
12
|
+
OPEN_TIMEOUT_DEFAULT = 60
|
13
|
+
|
14
|
+
attr_reader :configuration
|
15
|
+
attr_accessor :logger
|
16
|
+
|
17
|
+
BASE_REPO_URL = 'https://repo1.maven.org/maven2'
|
18
|
+
SEARCH_API = 'https://search.maven.org/solrsearch/select'
|
19
|
+
|
20
|
+
def initialize(configuration, _ssl_verify, logger)
|
21
|
+
@configuration = configuration
|
22
|
+
@configuration[:url] ||= BASE_REPO_URL
|
23
|
+
@logger = logger
|
24
|
+
end
|
25
|
+
|
26
|
+
def search_for_artifacts(coordinates)
|
27
|
+
artifact = parse_coordinates(coordinates)
|
28
|
+
params = {
|
29
|
+
q: "g:\"#{artifact[:group_id]}\" AND a:\"#{artifact[:artifact_id]}\"",
|
30
|
+
rows: 200,
|
31
|
+
wt: 'json',
|
32
|
+
core: 'gav'
|
33
|
+
}
|
34
|
+
query = params.map { |k, v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&')
|
35
|
+
url = "#{SEARCH_API}?#{query}"
|
36
|
+
|
37
|
+
response = Net::HTTP.get_response(URI(url))
|
38
|
+
raise "Search failed: #{response.code}" unless response.code.to_i == 200
|
39
|
+
|
40
|
+
json = JSON.parse(response.body)
|
41
|
+
docs = json['response']['docs']
|
42
|
+
search_versions = docs.map { |doc| doc['v'] }.uniq
|
43
|
+
|
44
|
+
# Apply when the artifact provided
|
45
|
+
if artifact[:artifact_id]
|
46
|
+
# Fetch metadata versions (incase the artifact is not indexed)
|
47
|
+
metadata_url = build_metadata_url(artifact)
|
48
|
+
metadata_versions = []
|
49
|
+
begin
|
50
|
+
metadata_response = Net::HTTP.get(URI(metadata_url))
|
51
|
+
if metadata_response.nil? || metadata_response.strip.empty?
|
52
|
+
logger.debug { "Empty metadata response for #{artifact[:artifact_id]}" }
|
53
|
+
else
|
54
|
+
begin
|
55
|
+
metadata_xml = REXML::Document.new(metadata_response)
|
56
|
+
metadata_xml.elements.each('//versioning/versions/version') do |version_node|
|
57
|
+
metadata_versions << version_node.text
|
58
|
+
end
|
59
|
+
rescue REXML::ParseException => e
|
60
|
+
logger.debug { "Malformed XML in metadata for #{artifact[:artifact_id]}: #{e.message}" }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
rescue StandardError => e
|
64
|
+
logger.debug { "Failed to fetch metadata for #{artifact[:artifact_id]}: #{e.message}" }
|
65
|
+
end
|
66
|
+
|
67
|
+
# Combine versions
|
68
|
+
search_versions = (search_versions + metadata_versions).uniq.sort_by do |v|
|
69
|
+
begin
|
70
|
+
Gem::Version.new(v)
|
71
|
+
rescue ArgumentError
|
72
|
+
v
|
73
|
+
end
|
74
|
+
end.reverse
|
75
|
+
|
76
|
+
artifacts_xml = '<searchNGResponse><data>'
|
77
|
+
search_versions.each do |version|
|
78
|
+
artifacts_xml += '<artifact>'
|
79
|
+
artifacts_xml += "<groupId>#{artifact[:group_id]}</groupId>"
|
80
|
+
artifacts_xml += "<artifactId>#{artifact[:artifact_id]}</artifactId>"
|
81
|
+
artifacts_xml += "<version>#{version}</version>"
|
82
|
+
artifacts_xml += '<repositoryId>central</repositoryId>'
|
83
|
+
artifacts_xml += '</artifact>'
|
84
|
+
end
|
85
|
+
else # Incase no artifact_id is provided for plugin search
|
86
|
+
artifacts_xml = '<searchNGResponse><data>'
|
87
|
+
docs.each do |doc|
|
88
|
+
artifacts_xml += '<artifact>'
|
89
|
+
artifacts_xml += "<groupId>#{doc['g']}</groupId>"
|
90
|
+
artifacts_xml += "<artifactId>#{doc['a']}</artifactId>"
|
91
|
+
artifacts_xml += "<version>#{doc['v']}</version>"
|
92
|
+
artifacts_xml += '<repositoryId>central</repositoryId>'
|
93
|
+
artifacts_xml += '</artifact>'
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
artifacts_xml += '</data></searchNGResponse>'
|
98
|
+
artifacts_xml
|
99
|
+
end
|
100
|
+
|
101
|
+
def get_artifact_info(coordinates)
|
102
|
+
coords = parse_coordinates(coordinates)
|
103
|
+
version = coords[:version]
|
104
|
+
if version.casecmp('latest').zero?
|
105
|
+
version = fetch_latest_version(coords)
|
106
|
+
coords = coords.merge(version: version)
|
107
|
+
end
|
108
|
+
_, versioned_artifact, coords = build_base_path_and_coords([coords[:group_id], coords[:artifact_id], coords[:extension], coords[:classifier], version].compact.join(':'))
|
109
|
+
sha1 = get_sha1([coords[:group_id], coords[:artifact_id], coords[:extension], coords[:classifier], version].compact.join(':'))
|
110
|
+
artifact_xml = '<artifact-resolution><data>'
|
111
|
+
artifact_xml += '<presentLocally>true</presentLocally>'
|
112
|
+
artifact_xml += "<groupId>#{coords[:group_id]}</groupId>"
|
113
|
+
artifact_xml += "<artifactId>#{coords[:artifact_id]}</artifactId>"
|
114
|
+
artifact_xml += "<version>#{coords[:version]}</version>"
|
115
|
+
artifact_xml += "<extension>#{coords[:extension]}</extension>"
|
116
|
+
artifact_xml += "<snapshot>#{!(coords[:version] =~ /-SNAPSHOT$/).nil?}</snapshot>"
|
117
|
+
artifact_xml += "<sha1>#{sha1}</sha1>"
|
118
|
+
artifact_xml += "<repositoryPath>/#{coords[:group_id].gsub('.', '/')}/#{versioned_artifact}</repositoryPath>"
|
119
|
+
artifact_xml += '</data></artifact-resolution>'
|
120
|
+
artifact_xml
|
121
|
+
end
|
122
|
+
|
123
|
+
def pull_artifact(coordinates, destination)
|
124
|
+
artifact = parse_coordinates(coordinates)
|
125
|
+
version = artifact[:version]
|
126
|
+
version = fetch_latest_version(artifact) if version.casecmp('latest').zero?
|
127
|
+
file_name = build_file_name(artifact[:artifact_id], version, artifact[:classifier], artifact[:extension])
|
128
|
+
download_url = build_download_url(artifact, version, file_name)
|
129
|
+
|
130
|
+
dest_path = File.join(File.expand_path(destination || '.'), file_name)
|
131
|
+
|
132
|
+
File.open(dest_path, 'wb') do |io|
|
133
|
+
io.write(Net::HTTP.get(URI(download_url)))
|
134
|
+
end
|
135
|
+
|
136
|
+
{
|
137
|
+
file_name: file_name,
|
138
|
+
file_path: File.expand_path(dest_path),
|
139
|
+
version: version,
|
140
|
+
size: File.size(File.expand_path(dest_path))
|
141
|
+
}
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
def parse_coordinates(coordinates)
|
147
|
+
raise 'Invalid coordinates' if coordinates.nil?
|
148
|
+
|
149
|
+
parts = coordinates.split(':')
|
150
|
+
|
151
|
+
{
|
152
|
+
group_id: parts[0],
|
153
|
+
artifact_id: parts[1],
|
154
|
+
extension: parts.size > 3 ? parts[2] : 'jar',
|
155
|
+
classifier: parts.size > 4 ? parts[3] : nil,
|
156
|
+
version: parts[-1]
|
157
|
+
}
|
158
|
+
end
|
159
|
+
|
160
|
+
def build_base_path_and_coords(coordinates)
|
161
|
+
coords = parse_coordinates(coordinates)
|
162
|
+
|
163
|
+
token_org_and_repo = URI.parse(configuration[:url]).path
|
164
|
+
|
165
|
+
[
|
166
|
+
"#{token_org_and_repo}/#{coords[:group_id].gsub('.', '/')}/#{coords[:artifact_id]}",
|
167
|
+
"#{coords[:version]}/#{coords[:artifact_id]}-#{coords[:version]}.#{coords[:extension]}",
|
168
|
+
coords
|
169
|
+
]
|
170
|
+
end
|
171
|
+
|
172
|
+
def get_sha1(coordinates)
|
173
|
+
base_path, versioned_artifact, = build_base_path_and_coords(coordinates)
|
174
|
+
endpoint = "#{base_path}/#{versioned_artifact}.sha1"
|
175
|
+
get_response_with_retries(nil, endpoint, nil)
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_response_with_retries(coordinates, endpoint, what_parameters)
|
179
|
+
logger.debug { "Fetching coordinates=#{coordinates}, endpoint=#{endpoint}, params=#{what_parameters}" }
|
180
|
+
response = get_response(coordinates, endpoint, what_parameters)
|
181
|
+
logger.debug { "Response body: #{response.body}" }
|
182
|
+
process_response_with_retries(response)
|
183
|
+
end
|
184
|
+
|
185
|
+
def process_response_with_retries(response)
|
186
|
+
case response.code
|
187
|
+
when '200'
|
188
|
+
response.body
|
189
|
+
when '301', '302', '307'
|
190
|
+
location = response['Location']
|
191
|
+
logger.debug { "Following redirect to #{location}" }
|
192
|
+
|
193
|
+
new_path = location.gsub!(configuration[:url], '')
|
194
|
+
if new_path.nil?
|
195
|
+
# Redirect to another domain (e.g. CDN)
|
196
|
+
get_raw_response_with_retries(location)
|
197
|
+
else
|
198
|
+
get_response_with_retries(nil, location, nil)
|
199
|
+
end
|
200
|
+
when '404'
|
201
|
+
raise StandardError, ERROR_MESSAGE_404
|
202
|
+
else
|
203
|
+
raise UnexpectedStatusCodeException, response.code
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def get_response(coordinates, endpoint, what_parameters)
|
208
|
+
http = build_http
|
209
|
+
query_params = build_query_params(coordinates, what_parameters) unless coordinates.nil?
|
210
|
+
endpoint = endpoint_with_params(endpoint, query_params) unless coordinates.nil?
|
211
|
+
request = Net::HTTP::Get.new(endpoint)
|
212
|
+
if configuration.key?(:username) && configuration.key?(:password)
|
213
|
+
request.basic_auth(configuration[:username], configuration[:password])
|
214
|
+
elsif configuration.key?(:token)
|
215
|
+
request['Authorization'] = "token #{configuration[:token]}"
|
216
|
+
end
|
217
|
+
|
218
|
+
logger.debug do
|
219
|
+
http.set_debug_output(logger)
|
220
|
+
"HTTP path: #{endpoint}"
|
221
|
+
end
|
222
|
+
|
223
|
+
http.request(request)
|
224
|
+
end
|
225
|
+
|
226
|
+
def build_metadata_url(artifact)
|
227
|
+
group_path = artifact[:group_id].tr('.', '/')
|
228
|
+
"#{BASE_REPO_URL}/#{group_path}/#{artifact[:artifact_id]}/maven-metadata.xml"
|
229
|
+
end
|
230
|
+
|
231
|
+
def fetch_latest_version(artifact)
|
232
|
+
xml = REXML::Document.new(Net::HTTP.get(URI(build_metadata_url(artifact))))
|
233
|
+
xml.elements['//versioning/latest']&.text || xml.elements['//version']&.text
|
234
|
+
end
|
235
|
+
|
236
|
+
def build_file_name(artifact_id, version, classifier, extension)
|
237
|
+
classifier ? "#{artifact_id}-#{version}-#{classifier}.#{extension}" : "#{artifact_id}-#{version}.#{extension}"
|
238
|
+
end
|
239
|
+
|
240
|
+
def build_download_url(artifact, version, file_name)
|
241
|
+
group_path = artifact[:group_id].tr('.', '/')
|
242
|
+
"#{BASE_REPO_URL}/#{group_path}/#{artifact[:artifact_id]}/#{version}/#{file_name}"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
data/lib/kpm/tasks.rb
CHANGED
@@ -245,7 +245,7 @@ module KPM
|
|
245
245
|
def search_for_plugins
|
246
246
|
all_plugins = KillbillPluginArtifact.versions(options[:overrides], options[:ssl_verify])
|
247
247
|
|
248
|
-
result =
|
248
|
+
result = String.new
|
249
249
|
all_plugins.each do |type, plugins|
|
250
250
|
result << "Available #{type} plugins:\n"
|
251
251
|
Hash[plugins.sort].each do |name, versions|
|
data/lib/kpm/version.rb
CHANGED
data/pom.xml
CHANGED
@@ -22,7 +22,7 @@
|
|
22
22
|
<modelVersion>4.0.0</modelVersion>
|
23
23
|
<groupId>org.kill-bill.billing.installer</groupId>
|
24
24
|
<artifactId>kpm</artifactId>
|
25
|
-
<version>0.
|
25
|
+
<version>0.12.0</version>
|
26
26
|
<packaging>pom</packaging>
|
27
27
|
<name>KPM</name>
|
28
28
|
<description>KPM: the Kill Bill Package Manager</description>
|
@@ -34,9 +34,6 @@ describe KPM::KillbillPluginArtifact do
|
|
34
34
|
expect(versions[:java]['analytics-plugin']).not_to be_nil
|
35
35
|
logging_plugin_versions = versions[:java]['analytics-plugin'].to_a
|
36
36
|
expect(logging_plugin_versions.size).to be >= 3
|
37
|
-
expect(logging_plugin_versions[0]).to eq '0.6.0'
|
38
|
-
expect(logging_plugin_versions[1]).to eq '0.7.0'
|
39
|
-
expect(logging_plugin_versions[2]).to eq '0.7.1'
|
40
37
|
end
|
41
38
|
|
42
39
|
private
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'rexml/document'
|
5
|
+
|
6
|
+
describe KPM::NexusFacade do
|
7
|
+
let(:logger) do
|
8
|
+
logger = ::Logger.new(STDOUT)
|
9
|
+
logger.level = Logger::DEBUG
|
10
|
+
logger
|
11
|
+
end
|
12
|
+
let(:test_version) { '0.24.15' }
|
13
|
+
let(:coordinates_map) do
|
14
|
+
{ version: test_version,
|
15
|
+
group_id: 'org.kill-bill.billing',
|
16
|
+
artifact_id: 'killbill',
|
17
|
+
packaging: 'pom',
|
18
|
+
classifier: nil }
|
19
|
+
end
|
20
|
+
let(:coordinates) { KPM::Coordinates.build_coordinates(coordinates_map) }
|
21
|
+
let(:nexus_remote) { described_class::MavenCentralApiCalls.new({}, nil, logger) }
|
22
|
+
|
23
|
+
context 'when pulling release artifact' do
|
24
|
+
it {
|
25
|
+
response = nil
|
26
|
+
expect { response = nexus_remote.get_artifact_info(coordinates) }.not_to raise_exception
|
27
|
+
parsed_doc = REXML::Document.new(response)
|
28
|
+
expect(parsed_doc.elements['//version'].text).to eq(test_version)
|
29
|
+
expect(parsed_doc.elements['//repositoryPath'].text).to eq("/org/kill-bill/billing/#{test_version}/killbill-#{test_version}.pom")
|
30
|
+
expect(parsed_doc.elements['//snapshot'].text).to eq('false')
|
31
|
+
}
|
32
|
+
|
33
|
+
it {
|
34
|
+
response = nil
|
35
|
+
destination = Dir.mktmpdir('artifact')
|
36
|
+
expect { response = nexus_remote.pull_artifact(coordinates, destination) }.not_to raise_exception
|
37
|
+
destination = File.join(File.expand_path(destination), response[:file_name])
|
38
|
+
parsed_pom = REXML::Document.new(File.read(destination))
|
39
|
+
expect(parsed_pom.elements['//groupId'].text).to eq('org.kill-bill.billing')
|
40
|
+
expect(parsed_pom.elements['//artifactId'].text).to eq('killbill-oss-parent')
|
41
|
+
expect(parsed_pom.elements['//version'].text).to eq('0.146.63')
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when getting artifact info' do
|
46
|
+
it 'returns artifact info in XML format' do
|
47
|
+
response = nil
|
48
|
+
expect { response = nexus_remote.get_artifact_info(coordinates) }.not_to raise_exception
|
49
|
+
parsed_doc = REXML::Document.new(response)
|
50
|
+
expect(parsed_doc.elements['//groupId'].text).to eq('org.kill-bill.billing')
|
51
|
+
expect(parsed_doc.elements['//artifactId'].text).to eq('killbill')
|
52
|
+
expect(parsed_doc.elements['//version'].text).to eq(test_version)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when searching for release artifact' do
|
57
|
+
it 'searches for artifacts and returns XML format' do
|
58
|
+
response = nil
|
59
|
+
expect do
|
60
|
+
response = nexus_remote.search_for_artifacts(coordinates)
|
61
|
+
end.not_to raise_exception
|
62
|
+
|
63
|
+
parsed_doc = REXML::Document.new(response)
|
64
|
+
expect(parsed_doc.elements['//groupId'].text).to eq('org.kill-bill.billing')
|
65
|
+
expect(parsed_doc.elements['//artifactId'].text).to eq('killbill')
|
66
|
+
expect(parsed_doc.elements['//version'].text).to eq(test_version)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kpm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.12.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kill Bill core team
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-08-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: highline
|
@@ -176,6 +176,7 @@ files:
|
|
176
176
|
- lib/kpm/nexus_helper/actions.rb
|
177
177
|
- lib/kpm/nexus_helper/cloudsmith_api_calls.rb
|
178
178
|
- lib/kpm/nexus_helper/github_api_calls.rb
|
179
|
+
- lib/kpm/nexus_helper/maven_central_api_calls.rb
|
179
180
|
- lib/kpm/nexus_helper/nexus_api_calls_v2.rb
|
180
181
|
- lib/kpm/nexus_helper/nexus_facade.rb
|
181
182
|
- lib/kpm/plugins_directory.rb
|
@@ -207,6 +208,7 @@ files:
|
|
207
208
|
- spec/kpm/remote/kaui_artifact_spec.rb
|
208
209
|
- spec/kpm/remote/killbill_plugin_artifact_spec.rb
|
209
210
|
- spec/kpm/remote/killbill_server_artifact_spec.rb
|
211
|
+
- spec/kpm/remote/maven_central_api_calls_spec.rb
|
210
212
|
- spec/kpm/remote/migrations_spec.rb
|
211
213
|
- spec/kpm/remote/nexus_facade_spec.rb
|
212
214
|
- spec/kpm/remote/tenant_config_spec.rb
|