fig 2.0.0.pre.alpha.4 → 2.0.0.pre.alpha.10
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/fig/operating_system.rb +3 -0
- data/lib/fig/protocol/artifactory.rb +389 -0
- data/lib/fig/spec_utils.rb +312 -0
- data/lib/fig/version.rb +1 -1
- data/spec/application_configuration_spec.rb +73 -0
- data/spec/command/clean_spec.rb +62 -0
- data/spec/command/command_line_vs_package_spec.rb +32 -0
- data/spec/command/dump_package_definition_spec.rb +104 -0
- data/spec/command/environment_variables_spec.rb +62 -0
- data/spec/command/grammar_asset_spec.rb +391 -0
- data/spec/command/grammar_command_spec.rb +88 -0
- data/spec/command/grammar_environment_variable_spec.rb +384 -0
- data/spec/command/grammar_retrieve_spec.rb +74 -0
- data/spec/command/grammar_spec.rb +87 -0
- data/spec/command/grammar_spec_helper.rb +23 -0
- data/spec/command/include_file_spec.rb +73 -0
- data/spec/command/listing_spec.rb +1574 -0
- data/spec/command/miscellaneous_spec.rb +145 -0
- data/spec/command/publish_local_and_updates_spec.rb +32 -0
- data/spec/command/publishing_retrieval_spec.rb +423 -0
- data/spec/command/publishing_spec.rb +596 -0
- data/spec/command/running_commands_spec.rb +354 -0
- data/spec/command/suppress_includes_spec.rb +65 -0
- data/spec/command/suppress_warning_include_statement_missing_version_spec.rb +134 -0
- data/spec/command/update_lock_response_spec.rb +47 -0
- data/spec/command/usage_errors_spec.rb +481 -0
- data/spec/command_options_spec.rb +184 -0
- data/spec/command_spec.rb +49 -0
- data/spec/deparser/v1_spec.rb +64 -0
- data/spec/environment_variables_spec.rb +91 -0
- data/spec/figrc_spec.rb +144 -0
- data/spec/parser_spec.rb +398 -0
- data/spec/protocol/artifactory_spec.rb +599 -0
- data/spec/repository_spec.rb +117 -0
- data/spec/runtime_environment_spec.rb +357 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/split_repo_url_spec.rb +190 -0
- data/spec/statement/asset_spec.rb +203 -0
- data/spec/statement/configuration_spec.rb +41 -0
- data/spec/support/formatters/seed_spitter.rb +12 -0
- data/spec/working_directory_maintainer_spec.rb +102 -0
- metadata +72 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ffb9d7bc5e0a1723f452acc24d3ed117f700814821c5f6b9be69258edeec4903
|
4
|
+
data.tar.gz: cff46d3b93314fc559e0e3ab549b472b9cdc1474363d758ef18c4020fca675b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 947d9c97f725fa69e074b0b580ba4698d4e824278c1ea2c71e036e5146030302de5319fe281171bc1727cd8a0194ace497edc731061fb9314317cb3aa52dacfe
|
7
|
+
data.tar.gz: a4c64c9fdf34e90543a790c3caf649cca749a74ef4b3b151902cfe931af17cc2a52d2f99dea12d090fcf8035c448fa5a8e5834b334683be4982882fe42da5e05
|
data/lib/fig/operating_system.rb
CHANGED
@@ -20,6 +20,7 @@ require 'fig/protocol/ftp'
|
|
20
20
|
require 'fig/protocol/http'
|
21
21
|
require 'fig/protocol/sftp'
|
22
22
|
require 'fig/protocol/ssh'
|
23
|
+
require 'fig/protocol/artifactory'
|
23
24
|
require 'fig/repository_error'
|
24
25
|
require 'fig/url'
|
25
26
|
require 'fig/user_input_error'
|
@@ -83,8 +84,10 @@ class Fig::OperatingSystem
|
|
83
84
|
@protocols['file'] = Fig::Protocol::File.new
|
84
85
|
@protocols['ftp'] = Fig::Protocol::FTP.new login
|
85
86
|
@protocols['http'] = Fig::Protocol::HTTP.new
|
87
|
+
@protocols['https'] = Fig::Protocol::HTTP.new
|
86
88
|
@protocols['sftp'] = Fig::Protocol::SFTP.new
|
87
89
|
@protocols['ssh'] = Fig::Protocol::SSH.new
|
90
|
+
@protocols['art'] = @protocols['artifactory'] = Fig::Protocol::Artifactory.new
|
88
91
|
end
|
89
92
|
|
90
93
|
def list(dir)
|
@@ -0,0 +1,389 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
require 'etc'
|
6
|
+
require 'thread'
|
7
|
+
require 'socket'
|
8
|
+
require 'digest'
|
9
|
+
require 'time'
|
10
|
+
|
11
|
+
require 'artifactory'
|
12
|
+
|
13
|
+
require 'fig/logging'
|
14
|
+
require 'fig/network_error'
|
15
|
+
require 'fig/package_descriptor'
|
16
|
+
require 'fig/protocol'
|
17
|
+
require 'fig/protocol/netrc_enabled'
|
18
|
+
|
19
|
+
|
20
|
+
module Fig; end
|
21
|
+
module Fig::Protocol; end
|
22
|
+
|
23
|
+
|
24
|
+
# file transfers/storage using https as the transport and artifactory as the backing store
|
25
|
+
class Fig::Protocol::Artifactory
|
26
|
+
include Fig::Protocol
|
27
|
+
include Fig::Protocol::NetRCEnabled
|
28
|
+
|
29
|
+
# Artifactory browser API endpoint (undocumented API)
|
30
|
+
BROWSER_API_PATH = 'ui/api/v1/ui/v2/nativeBrowser/'
|
31
|
+
|
32
|
+
# Default number of list entries to fetch on initial iteration
|
33
|
+
INITIAL_LIST_FETCH_SIZE = 20000
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
initialize_netrc
|
37
|
+
end
|
38
|
+
|
39
|
+
# must return a list of strings in the form <package_name>/<version>
|
40
|
+
def download_list(uri)
|
41
|
+
Fig::Logging.info("Downloading list of packages at #{uri}")
|
42
|
+
package_versions = []
|
43
|
+
|
44
|
+
begin
|
45
|
+
# Parse URI to extract base endpoint and repository key
|
46
|
+
# Expected format: https://artifacts.example.com/artifactory/repo-name/
|
47
|
+
parse_uri(uri) => { repo_key:, base_endpoint: }
|
48
|
+
|
49
|
+
# Create Artifactory client instance
|
50
|
+
authentication = get_authentication_for(uri.host, :prompt_for_login)
|
51
|
+
client_config = { endpoint: base_endpoint }
|
52
|
+
if authentication
|
53
|
+
client_config[:username] = authentication.username
|
54
|
+
client_config[:password] = authentication.password
|
55
|
+
end
|
56
|
+
client = ::Artifactory::Client.new(client_config)
|
57
|
+
|
58
|
+
# Use Artifactory browser API to list directories at repo root
|
59
|
+
list_url = URI.join(base_endpoint, BROWSER_API_PATH, "#{repo_key}/")
|
60
|
+
|
61
|
+
packages = get_all_artifactory_entries(list_url, client)
|
62
|
+
|
63
|
+
# Filter to only valid package names upfront
|
64
|
+
valid_packages = packages.select do |package_item|
|
65
|
+
package_item['folder'] &&
|
66
|
+
package_item['name'] =~ Fig::PackageDescriptor::COMPONENT_PATTERN
|
67
|
+
end
|
68
|
+
|
69
|
+
Fig::Logging.debug("Found #{valid_packages.size} valid packages, fetching versions concurrently...")
|
70
|
+
|
71
|
+
# Use concurrent requests to fetch version lists (major performance improvement)
|
72
|
+
package_versions = fetch_versions_concurrently(valid_packages, base_endpoint, repo_key, client_config)
|
73
|
+
|
74
|
+
rescue => e
|
75
|
+
# Follow FTP pattern: log error but don't fail completely
|
76
|
+
Fig::Logging.debug("Could not retrieve package list from #{uri}: #{e.message}")
|
77
|
+
end
|
78
|
+
|
79
|
+
return package_versions.sort
|
80
|
+
end
|
81
|
+
|
82
|
+
def download(art_uri, path, prompt_for_login)
|
83
|
+
Fig::Logging.info("Downloading from artifactory: #{art_uri}")
|
84
|
+
|
85
|
+
uri = httpify_uri(art_uri)
|
86
|
+
|
87
|
+
# Log equivalent curl command for debugging
|
88
|
+
authentication = get_authentication_for(uri.host, prompt_for_login)
|
89
|
+
if authentication
|
90
|
+
Fig::Logging.debug("Equivalent curl: curl -u #{authentication.username}:*** -o '#{path}' '#{uri}'")
|
91
|
+
else
|
92
|
+
Fig::Logging.debug("Equivalent curl: curl -o '#{path}' '#{uri}'")
|
93
|
+
end
|
94
|
+
|
95
|
+
::File.open(path, 'wb') do |file|
|
96
|
+
file.binmode
|
97
|
+
|
98
|
+
begin
|
99
|
+
download_via_http_get(uri.to_s, file)
|
100
|
+
rescue SystemCallError => error
|
101
|
+
Fig::Logging.debug error.message
|
102
|
+
raise Fig::FileNotFoundError.new error.message, uri
|
103
|
+
rescue SocketError => error
|
104
|
+
Fig::Logging.debug error.message
|
105
|
+
raise Fig::FileNotFoundError.new error.message, uri
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return true
|
110
|
+
end
|
111
|
+
|
112
|
+
def upload(local_file, uri)
|
113
|
+
Fig::Logging.info("Uploading #{local_file} to artifactory at #{uri}")
|
114
|
+
|
115
|
+
begin
|
116
|
+
parse_uri(uri) => { repo_key:, base_endpoint:, target_path: }
|
117
|
+
|
118
|
+
# Configure Artifactory gem globally - unlike other methods that can use client instances,
|
119
|
+
# the artifact.upload() method ignores the client: parameter and only uses global config.
|
120
|
+
# This is a limitation of the artifactory gem's upload implementation.
|
121
|
+
authentication = get_authentication_for(uri.host, :prompt_for_login)
|
122
|
+
::Artifactory.configure do |config|
|
123
|
+
config.endpoint = base_endpoint
|
124
|
+
if authentication
|
125
|
+
config.username = authentication.username
|
126
|
+
config.password = authentication.password
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Log equivalent curl command for debugging
|
131
|
+
if authentication
|
132
|
+
Fig::Logging.debug("Equivalent curl: curl -u #{authentication.username}:*** -T '#{local_file}' '#{uri}'")
|
133
|
+
else
|
134
|
+
Fig::Logging.debug("Equivalent curl: curl -T '#{local_file}' '#{uri}'")
|
135
|
+
end
|
136
|
+
|
137
|
+
# Create artifact and upload
|
138
|
+
artifact = ::Artifactory::Resource::Artifact.new(local_path: local_file)
|
139
|
+
|
140
|
+
# Collect metadata for upload
|
141
|
+
metadata = collect_upload_metadata(local_file, target_path, uri)
|
142
|
+
|
143
|
+
# Upload with metadata (no client parameter needed - uses global config)
|
144
|
+
artifact.upload(repo_key, target_path, metadata)
|
145
|
+
|
146
|
+
Fig::Logging.info("Successfully uploaded #{local_file} to #{uri}")
|
147
|
+
|
148
|
+
rescue ArgumentError => e
|
149
|
+
# Let ArgumentError bubble up for invalid URIs
|
150
|
+
raise e
|
151
|
+
rescue => e
|
152
|
+
Fig::Logging.debug("Upload failed: #{e.message}")
|
153
|
+
raise Fig::NetworkError.new("Failed to upload #{local_file} to #{uri}: #{e.message}")
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# we can know this most of the time with the stat api
|
158
|
+
def path_up_to_date?(uri, path, prompt_for_login)
|
159
|
+
Fig::Logging.info("Checking if #{path} is up to date at #{uri}")
|
160
|
+
|
161
|
+
begin
|
162
|
+
parse_uri(uri) => { repo_key:, base_endpoint:, target_path: }
|
163
|
+
|
164
|
+
|
165
|
+
# Create Artifactory client instance (same as upload method)
|
166
|
+
authentication = get_authentication_for(uri.host, prompt_for_login)
|
167
|
+
client_config = { endpoint: base_endpoint }
|
168
|
+
if authentication
|
169
|
+
client_config[:username] = authentication.username
|
170
|
+
client_config[:password] = authentication.password
|
171
|
+
end
|
172
|
+
client = ::Artifactory::Client.new(client_config)
|
173
|
+
|
174
|
+
# use storage api instead of search - more reliable for virtual repos
|
175
|
+
storage_url = "/api/storage/#{repo_key}/#{target_path}"
|
176
|
+
|
177
|
+
response = client.get(storage_url)
|
178
|
+
|
179
|
+
# compare sizes first
|
180
|
+
if response['size'] != ::File.size(path)
|
181
|
+
return false
|
182
|
+
end
|
183
|
+
|
184
|
+
# compare modification times
|
185
|
+
remote_mtime = Time.parse(response['lastModified'])
|
186
|
+
local_mtime = ::File.mtime(path)
|
187
|
+
|
188
|
+
if remote_mtime <= local_mtime
|
189
|
+
return true
|
190
|
+
end
|
191
|
+
|
192
|
+
return false
|
193
|
+
rescue => error
|
194
|
+
Fig::Logging.debug "Error checking if #{path} is up to date: #{error.message}"
|
195
|
+
return nil
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def httpify_uri(art_uri)
|
202
|
+
# if we got into the artifactory protocol, we get to assume that the URI
|
203
|
+
# is related to artifactory, which also means we can be assured that the
|
204
|
+
# only scheme that will work is 'https' rather than the faux scheme used
|
205
|
+
# to signal that the URL is artifactory protocol.
|
206
|
+
|
207
|
+
# this is kind of weird b/c `art_uri` will likely be URI::Generic and
|
208
|
+
# everything downstream of here is going to want the type for the URL to
|
209
|
+
# be URI::HTTPS. So, we hack the scheme for the input to `https` then
|
210
|
+
# parse that string to form the https-based URI with the correct type.
|
211
|
+
art_uri.scheme = 'https'
|
212
|
+
URI(art_uri.to_s)
|
213
|
+
end
|
214
|
+
|
215
|
+
def parse_uri(art_uri)
|
216
|
+
uri = httpify_uri(art_uri)
|
217
|
+
|
218
|
+
uri_path = uri.path.chomp('/')
|
219
|
+
path_parts = uri_path.split('/').reject(&:empty?)
|
220
|
+
|
221
|
+
# Find artifactory in the path and split accordingly
|
222
|
+
artifactory_index = path_parts.index('artifactory')
|
223
|
+
raise ArgumentError, "URI must contain 'artifactory' in path: #{uri}" unless artifactory_index
|
224
|
+
|
225
|
+
repo_key = path_parts[artifactory_index + 1]
|
226
|
+
raise ArgumentError, "No repository key found in URI: #{uri}" unless repo_key
|
227
|
+
|
228
|
+
# Everything after repo_key is the target path within the repository
|
229
|
+
target_path = path_parts[(artifactory_index + 2)..-1]&.join('/') || ''
|
230
|
+
|
231
|
+
# Base endpoint includes everything up to and including /artifactory
|
232
|
+
artifactory_path = path_parts[0..artifactory_index].join('/')
|
233
|
+
the_port = uri.port != uri.default_port ? ":#{uri.port}" : ""
|
234
|
+
base_endpoint = "#{uri.scheme}://#{uri.host}#{the_port}/#{artifactory_path}"
|
235
|
+
|
236
|
+
{ repo_key:, base_endpoint:, target_path: }
|
237
|
+
end
|
238
|
+
|
239
|
+
# Collect metadata for file upload, using fig. prefix instead of upload. prefix
|
240
|
+
def collect_upload_metadata(local_file, target_path, uri)
|
241
|
+
file_stat = ::File.stat(local_file)
|
242
|
+
|
243
|
+
metadata = {
|
244
|
+
# Basic file information
|
245
|
+
'fig.original_path' => local_file,
|
246
|
+
'fig.target_path' => target_path,
|
247
|
+
'fig.file_size' => file_stat.size.to_s,
|
248
|
+
'fig.file_mtime' => file_stat.mtime.iso8601,
|
249
|
+
|
250
|
+
# Upload context
|
251
|
+
'fig.hostname' => Socket.gethostname,
|
252
|
+
'fig.login' => ENV['USER'] || ENV['USERNAME'] || 'unknown',
|
253
|
+
'fig.timestamp' => Time.now.iso8601,
|
254
|
+
'fig.epoch' => Time.now.to_i.to_s,
|
255
|
+
|
256
|
+
# Tool information
|
257
|
+
'fig.tool' => 'fig-artifactory-protocol',
|
258
|
+
'fig.uri' => uri.to_s,
|
259
|
+
|
260
|
+
# Checksums for integrity
|
261
|
+
'fig.sha1' => Digest::SHA1.file(local_file).hexdigest,
|
262
|
+
'fig.md5' => Digest::MD5.file(local_file).hexdigest,
|
263
|
+
|
264
|
+
# TODO: Add package/version metadata from URI path or local_file decoration
|
265
|
+
# TODO: Support user-injected metadata via environment variables or callbacks
|
266
|
+
}
|
267
|
+
|
268
|
+
Fig::Logging.debug("Upload metadata: #{metadata.keys.join(', ')}")
|
269
|
+
metadata
|
270
|
+
end
|
271
|
+
|
272
|
+
# Fetch version lists for packages concurrently for major performance improvement
|
273
|
+
# Reduces ~2700 sequential API calls to concurrent batches
|
274
|
+
def fetch_versions_concurrently(valid_packages, base_endpoint, repo_key, client_config)
|
275
|
+
# Scale thread count based on CPU cores, with reasonable bounds
|
276
|
+
# Testing showed ~25 threads optimal for most servers, so use 3x CPU cores as default
|
277
|
+
default_threads = [Etc.nprocessors * 3, 50].min # cap at 50 to avoid overwhelming servers
|
278
|
+
max_threads = ENV['FIG_ARTIFACTORY_THREADS']&.to_i || default_threads
|
279
|
+
|
280
|
+
Fig::Logging.debug("Using #{max_threads} threads for concurrent version fetching (#{Etc.nprocessors} CPU cores detected)")
|
281
|
+
|
282
|
+
package_versions = []
|
283
|
+
package_versions_mutex = Mutex.new
|
284
|
+
work_queue = Queue.new
|
285
|
+
|
286
|
+
# Add all packages to work queue
|
287
|
+
valid_packages.each { |pkg| work_queue << pkg }
|
288
|
+
|
289
|
+
# Create worker threads
|
290
|
+
threads = []
|
291
|
+
max_threads.times do
|
292
|
+
threads << Thread.new do
|
293
|
+
# Each thread gets its own client to avoid connection issues
|
294
|
+
thread_client = ::Artifactory::Client.new(client_config)
|
295
|
+
|
296
|
+
while !work_queue.empty?
|
297
|
+
begin
|
298
|
+
package_item = work_queue.pop(true) # non-blocking pop
|
299
|
+
rescue ThreadError
|
300
|
+
break # queue is empty
|
301
|
+
end
|
302
|
+
|
303
|
+
package_name = package_item['name']
|
304
|
+
|
305
|
+
begin
|
306
|
+
package_list_url = URI.join(base_endpoint, BROWSER_API_PATH, "#{repo_key}/", "#{package_name}/")
|
307
|
+
versions = get_all_artifactory_entries(package_list_url, thread_client)
|
308
|
+
|
309
|
+
local_package_versions = []
|
310
|
+
versions.each do |version_item|
|
311
|
+
next unless version_item['folder']
|
312
|
+
|
313
|
+
version_name = version_item['name']
|
314
|
+
next unless version_name =~ Fig::PackageDescriptor::COMPONENT_PATTERN
|
315
|
+
|
316
|
+
local_package_versions << "#{package_name}/#{version_name}"
|
317
|
+
end
|
318
|
+
|
319
|
+
# Thread-safe addition to results
|
320
|
+
package_versions_mutex.synchronize do
|
321
|
+
package_versions.concat(local_package_versions)
|
322
|
+
end
|
323
|
+
|
324
|
+
rescue => e
|
325
|
+
# Follow FTP pattern: ignore permission errors and continue processing
|
326
|
+
Fig::Logging.debug("Could not list versions for package #{package_name}: #{e.message}")
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
# Wait for all threads to complete
|
333
|
+
threads.each(&:join)
|
334
|
+
|
335
|
+
package_versions
|
336
|
+
end
|
337
|
+
|
338
|
+
# Get all entries from Artifactory browser API with pagination support
|
339
|
+
# Returns array of all entries, handling continueState pagination
|
340
|
+
def get_all_artifactory_entries(base_url, client)
|
341
|
+
record_num = ENV['FIG_ARTIFACTORY_PAGESIZE']&.to_i || INITIAL_LIST_FETCH_SIZE
|
342
|
+
|
343
|
+
Fig::Logging.debug(">> getting art initial #{record_num} entries from #{base_url}...")
|
344
|
+
|
345
|
+
loop do
|
346
|
+
# Build URL with recordNum parameter
|
347
|
+
url = URI(base_url.to_s)
|
348
|
+
url.query = "recordNum=#{record_num}"
|
349
|
+
|
350
|
+
response = client.get(url)
|
351
|
+
entries = response['data'] || []
|
352
|
+
|
353
|
+
# Check if there are more entries to fetch
|
354
|
+
continue_state = response['continueState']
|
355
|
+
Fig::Logging.debug(">> continue_state is #{continue_state}...")
|
356
|
+
return entries if continue_state.nil? || continue_state.to_i < 0
|
357
|
+
|
358
|
+
# Use continueState as the recordNum for the next request
|
359
|
+
record_num = continue_state
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# swiped directly from http.rb; if no changes are required, then consider refactoring
|
364
|
+
def download_via_http_get(uri_string, file, redirection_limit = 10)
|
365
|
+
if redirection_limit < 1
|
366
|
+
Fig::Logging.debug 'Too many HTTP redirects.'
|
367
|
+
raise Fig::FileNotFoundError.new 'Too many HTTP redirects.', uri_string
|
368
|
+
end
|
369
|
+
|
370
|
+
response = Net::HTTP.get_response(URI(uri_string))
|
371
|
+
|
372
|
+
case response
|
373
|
+
when Net::HTTPSuccess then
|
374
|
+
file.write(response.body)
|
375
|
+
when Net::HTTPRedirection then
|
376
|
+
location = response['location']
|
377
|
+
Fig::Logging.debug "Redirecting to #{location}."
|
378
|
+
download_via_http_get(location, file, redirection_limit - 1)
|
379
|
+
else
|
380
|
+
Fig::Logging.debug "Download failed: #{response.code} #{response.message}."
|
381
|
+
raise Fig::FileNotFoundError.new(
|
382
|
+
"Download failed: #{response.code} #{response.message}.", uri_string
|
383
|
+
)
|
384
|
+
end
|
385
|
+
|
386
|
+
return
|
387
|
+
end
|
388
|
+
|
389
|
+
end
|