artifactory-cleaner 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.idea/.gitignore +2 -0
- data/.idea/checkstyle-idea.xml +16 -0
- data/.idea/dictionaries/jgitlin.xml +7 -0
- data/.idea/misc.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rspec +3 -0
- data/.rspec_status +39 -0
- data/.travis.yml +6 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +12 -0
- data/README.md +73 -0
- data/Rakefile +20 -0
- data/artifactory-cleaner.gemspec +43 -0
- data/artifactory-cleaner.iml +9 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/doc/rdoc/Artifactory.html +94 -0
- data/doc/rdoc/Artifactory/Cleaner.html +108 -0
- data/doc/rdoc/Artifactory/Cleaner/ArtifactBucket.html +504 -0
- data/doc/rdoc/Artifactory/Cleaner/ArtifactBucketCollection.html +570 -0
- data/doc/rdoc/Artifactory/Cleaner/ArtifactFilter.html +712 -0
- data/doc/rdoc/Artifactory/Cleaner/ArtifactFilterRule.html +519 -0
- data/doc/rdoc/Artifactory/Cleaner/CLI.html +625 -0
- data/doc/rdoc/Artifactory/Cleaner/Controller.html +1014 -0
- data/doc/rdoc/Artifactory/Cleaner/DiscoveredArtifact.html +400 -0
- data/doc/rdoc/Artifactory/Cleaner/DiscoveryWorker.html +466 -0
- data/doc/rdoc/Artifactory/Cleaner/Error.html +101 -0
- data/doc/rdoc/Artifactory/Cleaner/SpecHelpers.html +190 -0
- data/doc/rdoc/Artifactory/Cleaner/Util.html +157 -0
- data/doc/rdoc/CODE_OF_CONDUCT_md.html +228 -0
- data/doc/rdoc/Float.html +94 -0
- data/doc/rdoc/Gemfile.html +144 -0
- data/doc/rdoc/Gemfile_lock.html +217 -0
- data/doc/rdoc/Object.html +112 -0
- data/doc/rdoc/README_md.html +241 -0
- data/doc/rdoc/Rakefile.html +151 -0
- data/doc/rdoc/artifactory-cleaner_gemspec.html +173 -0
- data/doc/rdoc/artifactory-cleaner_iml.html +139 -0
- data/doc/rdoc/bin/setup.html +134 -0
- data/doc/rdoc/created.rid +219 -0
- data/doc/rdoc/css/fonts.css +167 -0
- data/doc/rdoc/css/rdoc.css +590 -0
- data/doc/rdoc/filterlist_yaml.html +149 -0
- data/doc/rdoc/filters/clean-amzn_yaml.html +133 -0
- data/doc/rdoc/filters/snapshots_yaml.html +137 -0
- data/doc/rdoc/filters/test-filter_yaml.html +137 -0
- data/doc/rdoc/filters/yum-test_yaml.html +141 -0
- data/doc/rdoc/fonts/Lato-Light.ttf +0 -0
- data/doc/rdoc/fonts/Lato-LightItalic.ttf +0 -0
- data/doc/rdoc/fonts/Lato-Regular.ttf +0 -0
- data/doc/rdoc/fonts/Lato-RegularItalic.ttf +0 -0
- data/doc/rdoc/fonts/SourceCodePro-Bold.ttf +0 -0
- data/doc/rdoc/fonts/SourceCodePro-Regular.ttf +0 -0
- data/doc/rdoc/images/add.png +0 -0
- data/doc/rdoc/images/arrow_up.png +0 -0
- data/doc/rdoc/images/brick.png +0 -0
- data/doc/rdoc/images/brick_link.png +0 -0
- data/doc/rdoc/images/bug.png +0 -0
- data/doc/rdoc/images/bullet_black.png +0 -0
- data/doc/rdoc/images/bullet_toggle_minus.png +0 -0
- data/doc/rdoc/images/bullet_toggle_plus.png +0 -0
- data/doc/rdoc/images/date.png +0 -0
- data/doc/rdoc/images/delete.png +0 -0
- data/doc/rdoc/images/find.png +0 -0
- data/doc/rdoc/images/loadingAnimation.gif +0 -0
- data/doc/rdoc/images/macFFBgHack.png +0 -0
- data/doc/rdoc/images/package.png +0 -0
- data/doc/rdoc/images/page_green.png +0 -0
- data/doc/rdoc/images/page_white_text.png +0 -0
- data/doc/rdoc/images/page_white_width.png +0 -0
- data/doc/rdoc/images/plugin.png +0 -0
- data/doc/rdoc/images/ruby.png +0 -0
- data/doc/rdoc/images/tag_blue.png +0 -0
- data/doc/rdoc/images/tag_green.png +0 -0
- data/doc/rdoc/images/transparent.png +0 -0
- data/doc/rdoc/images/wrench.png +0 -0
- data/doc/rdoc/images/wrench_orange.png +0 -0
- data/doc/rdoc/images/zoom.png +0 -0
- data/doc/rdoc/index.html +166 -0
- data/doc/rdoc/js/darkfish.js +161 -0
- data/doc/rdoc/js/jquery.js +4 -0
- data/doc/rdoc/js/navigation.js +141 -0
- data/doc/rdoc/js/navigation.js.gz +0 -0
- data/doc/rdoc/js/search.js +109 -0
- data/doc/rdoc/js/search_index.js +1 -0
- data/doc/rdoc/js/search_index.js.gz +0 -0
- data/doc/rdoc/js/searcher.js +229 -0
- data/doc/rdoc/js/searcher.js.gz +0 -0
- data/doc/rdoc/results/archive-test-4_log.html +762 -0
- data/doc/rdoc/results/buckets-2020-01-31_txt.html +233 -0
- data/doc/rdoc/results/clean-test-2_log.html +598 -0
- data/doc/rdoc/results/clean-test-3_log.html +128 -0
- data/doc/rdoc/results/clean-test-5_log.html +2721 -0
- data/doc/rdoc/results/clean-test-6_log.html +135 -0
- data/doc/rdoc/results/clean-test-7_log.html +137 -0
- data/doc/rdoc/results/clean-test-8-real_log.html +131 -0
- data/doc/rdoc/results/clean-test-9_log.html +131 -0
- data/doc/rdoc/results/clean-test1_log.html +1759 -0
- data/doc/rdoc/results/yum-test_2020-01-31_log.html +2854 -0
- data/doc/rdoc/results/yum-test_dry-run_log.html +1074 -0
- data/doc/rdoc/table_of_contents.html +581 -0
- data/exe/artifactory-cleaner +12 -0
- data/lib/artifactory/cleaner.rb +17 -0
- data/lib/artifactory/cleaner/artifact_bucket.rb +102 -0
- data/lib/artifactory/cleaner/artifact_bucket_collection.rb +118 -0
- data/lib/artifactory/cleaner/artifact_filter.rb +146 -0
- data/lib/artifactory/cleaner/artifact_filter_rule.rb +81 -0
- data/lib/artifactory/cleaner/cli.rb +415 -0
- data/lib/artifactory/cleaner/controller.rb +466 -0
- data/lib/artifactory/cleaner/discovered_artifact.rb +71 -0
- data/lib/artifactory/cleaner/discovery_worker.rb +126 -0
- data/lib/artifactory/cleaner/util.rb +21 -0
- data/lib/artifactory/cleaner/version.rb +7 -0
- metadata +252 -0
@@ -0,0 +1,415 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'yaml'
|
3
|
+
require 'sysexits'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module Artifactory
|
7
|
+
module Cleaner
|
8
|
+
|
9
|
+
##
|
10
|
+
# Command Line Interface class, powers the artifactory-cleaner terminal command
|
11
|
+
# ---
|
12
|
+
# A single Artifactory::Cleaner::CLI instance is created by the bin/artifactory_cleaner command for parsing options and
|
13
|
+
# executing the command specified by the user. The Artifactory::Cleaner::CLI uses {Thor}[https://github.com/erikhuda/thor]
|
14
|
+
# to provide git command/subcommand style ARGV parsing
|
15
|
+
#
|
16
|
+
# @see exe/artifactory-cleaner
|
17
|
+
#
|
18
|
+
# @see https://github.com/erikhuda/thor
|
19
|
+
class CLI < Thor
|
20
|
+
class_option :verbose, :aliases => %w(-v), :type => :boolean, :desc => "Verbose mode; print additional information to STDERR"
|
21
|
+
class_option :conf_file, :aliases => %w(-c), :type => :string, :desc => "Provide a path to configuration file with endpoint and API key"
|
22
|
+
class_option :endpoint, :aliases => %w(-e), :type => :string, :desc => "Artifatcory endpoint URL"
|
23
|
+
class_option :api_key, :aliases => %w(-k), :type => :string, :desc => "Artifactory API key"
|
24
|
+
|
25
|
+
RepoTableCol = Struct.new(:method, :heading, :only)
|
26
|
+
def self.repo_table_cols
|
27
|
+
[
|
28
|
+
RepoTableCol.new(:key, 'ID', nil),
|
29
|
+
RepoTableCol.new(:package_type, 'Type', nil),
|
30
|
+
RepoTableCol.new(:rclass, 'Class', :local),
|
31
|
+
RepoTableCol.new(:url, 'URL', :remote),
|
32
|
+
RepoTableCol.new(:description, 'Description', nil),
|
33
|
+
RepoTableCol.new(:notes, 'Notes', nil),
|
34
|
+
RepoTableCol.new(:blacked_out?, 'Blacked Out', nil),
|
35
|
+
RepoTableCol.new(:yum_root_depth, 'YUM Root Depth', nil),
|
36
|
+
RepoTableCol.new(:checksum_policy_type, 'Checksum Policy', nil),
|
37
|
+
RepoTableCol.new(:includes_pattern, 'Includes Pattern', nil),
|
38
|
+
RepoTableCol.new(:excludes_pattern, 'Excludes Pattern', nil),
|
39
|
+
RepoTableCol.new(:handle_releases, 'Releases', nil),
|
40
|
+
RepoTableCol.new(:handle_snapshots, 'Snapshots', nil),
|
41
|
+
RepoTableCol.new(:property_sets, 'Property Sets', nil),
|
42
|
+
RepoTableCol.new(:repo_layout_ref, 'Layout', nil),
|
43
|
+
RepoTableCol.new(:repositories, 'Included Repos', nil),
|
44
|
+
RepoTableCol.new(:inspect, 'Inspection'),
|
45
|
+
]
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.default_repo_table_cols
|
49
|
+
[:key, :package_type, :rclass, :url, :description]
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Constructor for a new CLI interface
|
54
|
+
def initialize(*args)
|
55
|
+
super
|
56
|
+
@config = {}
|
57
|
+
begin
|
58
|
+
load_conf_file options.conf_file if options.conf_file
|
59
|
+
rescue => ex
|
60
|
+
STDERR.puts "Unable to load config from #{options.conf_file}: #{ex}"
|
61
|
+
exit Sysexits::EX_DATAERR
|
62
|
+
end
|
63
|
+
@artifactory_config = {
|
64
|
+
endpoint: options[:endpoint] || @config['endpoint'],
|
65
|
+
api_key: options[:api_key] || @config['api-key'],
|
66
|
+
}
|
67
|
+
@repo_table_cols = Artifactory::Cleaner::CLI.repo_table_cols
|
68
|
+
#invoke :create_controller
|
69
|
+
create_controller
|
70
|
+
end
|
71
|
+
|
72
|
+
desc "version", "Show version information"
|
73
|
+
##
|
74
|
+
# Show version information
|
75
|
+
def version
|
76
|
+
STDERR.puts "Artifactory::Cleaner version #{Artifactory::Cleaner::VERSION}"
|
77
|
+
STDERR.puts "Copyright (C) 2020 Pinnacle 21, inc. All Rights Reserved"
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "list-repos", "List all available repos"
|
81
|
+
option :details, :type => :boolean
|
82
|
+
option :no_headers, :aliases => %w(-H), :type => :boolean, :desc => "Used for scripting mode. Do not print headers and separate fields by a single tab instead of arbitrary white space."
|
83
|
+
option :output, :aliases => %w(-o), :type => :string, :desc => " A comma-separated list of properties to display. Available properties are: #{(Artifactory::Cleaner::CLI.repo_table_cols.map &:method ).join(',')}"
|
84
|
+
option :local, :type => :boolean, :default => true, :desc => "Include local repositories"
|
85
|
+
option :remote, :type => :boolean, :default => false, :desc => "Include remote (replication) repositories"
|
86
|
+
option :virtual, :type => :boolean, :default => false, :desc => "Include virtual (union) repositories"
|
87
|
+
##
|
88
|
+
# List all available repos
|
89
|
+
def list_repos()
|
90
|
+
repo_info_table = []
|
91
|
+
repos = @controller.discover_repos
|
92
|
+
repo_kinds = []
|
93
|
+
repo_kinds << :local if options.local?
|
94
|
+
repo_kinds << :remote if options.remote?
|
95
|
+
repo_kinds << :virtual if options.virtual?
|
96
|
+
include_cols = get_repo_cols(repo_kinds)
|
97
|
+
repos[:local].each {|k, r| repo_info_table << repo_cols(r, include_cols)} if options.local?
|
98
|
+
repos[:remote].each {|k, r| repo_info_table << repo_cols(r, include_cols)} if options.remote?
|
99
|
+
repos[:virtual].each {|k, r| repo_info_table << repo_cols(r, include_cols)} if options.virtual?
|
100
|
+
print_repo_list repo_info_table, include_cols
|
101
|
+
end
|
102
|
+
|
103
|
+
desc "usage-report", "Analyze usage and report where space is used"
|
104
|
+
option :details, :type => :boolean, :desc => "Produce a detailed report listing all artifacts"
|
105
|
+
option :buckets, :type => :string, :desc => "Comma separated list of bucket sizes (age in days) to group artifacts by"
|
106
|
+
option :repos, :type => :array, :desc => "List of repos to analyze; will analyze all repos if omitted"
|
107
|
+
option :from, :type => :string, :default => (Time.now - 2*365*24*3600).to_s
|
108
|
+
option :to, :type => :string, :default => (Time.now).to_s
|
109
|
+
option :threads, :type => :numeric, :default => 4
|
110
|
+
##
|
111
|
+
# Analyze usage and report where space is used
|
112
|
+
def usage_report
|
113
|
+
begin
|
114
|
+
from = Time.parse(options.from)
|
115
|
+
to = Time.parse(options.to)
|
116
|
+
rescue => ex
|
117
|
+
STDERR.puts "Unable to parse time format. Please use: YYYY-MM-DD HH:II:SS"
|
118
|
+
STDERR.puts ex
|
119
|
+
exit Sysexits::EX_USAGE
|
120
|
+
end
|
121
|
+
|
122
|
+
begin
|
123
|
+
STDERR.puts "[DEBUG] controller.bucketize_artifacts from #{from} to #{to} repos #{options.repos}" if options.verbose?
|
124
|
+
buckets = @controller.bucketize_artifacts(
|
125
|
+
from: from,
|
126
|
+
to: to,
|
127
|
+
repos: options.repos,
|
128
|
+
threads: options.threads,
|
129
|
+
)
|
130
|
+
|
131
|
+
@controller.bucketized_artifact_report(buckets).each { |l| STDERR.puts l }
|
132
|
+
if options.details?
|
133
|
+
puts "# Detailed Bucket Report:"
|
134
|
+
puts "buckets:"
|
135
|
+
buckets.each do |bucket|
|
136
|
+
puts "#-- #{bucket.length} artifacts between #{bucket.min} and #{bucket.max} days old repo_info_table #{bucket.filesize} bytes --"
|
137
|
+
puts " - min: #{bucket.min} # days old"
|
138
|
+
puts " max: #{bucket.max} # days old"
|
139
|
+
if bucket.empty?
|
140
|
+
puts " artifacts: []"
|
141
|
+
else
|
142
|
+
puts " artifacts:"
|
143
|
+
bucket.each { |pkg| puts " - #{@controller.yaml_format(pkg,6)}" }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
rescue => err
|
148
|
+
STDERR.puts "An exception occured while generating the usage report: #{err}"
|
149
|
+
STDERR.puts err.full_message
|
150
|
+
STDERR.puts "Caused by: #{err.cause.full_message}" if err.cause
|
151
|
+
Pry::rescued(err) if defined?(Pry::rescue)
|
152
|
+
exit Sysexits::EX_UNAVAILABLE
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
desc "archive", "Download artifacts meeting specific criteria"
|
157
|
+
option :dry_run, :aliases => '-n', :type => :boolean, :desc => "Do not actually download anything, only show what actions would have been taken"
|
158
|
+
option :repos, :type => :array, :desc => "List of repos to download from; will download from all repos if omitted"
|
159
|
+
option :from, :type => :string, :default => (Time.now - 2*365*24*3600).to_s, :desc => "Earliest date to include in search; defaults to 2 years ago"
|
160
|
+
option :created_before, :type => :string, :desc => "Archive artifacts with a created date earlier than the provided value"
|
161
|
+
option :modified_before, :type => :string, :desc => "Archive artifacts with a last modified date earlier than the provided value"
|
162
|
+
option :downloaded_before, :type => :string, :desc => "Archive artifacts with a last downloaded date earlier than the provided value"
|
163
|
+
option :last_used_before, :type => :string, :desc => "Archive artifacts which were created, last modified and last downloaded before the provided date"
|
164
|
+
option :threads, :type => :numeric, :default => 4, :desc => "Number of threads to use for fetching artifact info"
|
165
|
+
option :archive_to, :type => :string, :desc => "Save artifacts to the provided path before deletion"
|
166
|
+
option :filter, :type => :string, :desc => "Specify a YAML file containing filter rules to use"
|
167
|
+
##
|
168
|
+
# Download artifacts meeting specific criteria
|
169
|
+
#
|
170
|
+
# **WARNING:** This method will cause the `last_downloaded` property of all the matching artifacts to be updated;
|
171
|
+
# therefore, using this method with a `last_used_before` switch may not be idempotent as it will cause the set of
|
172
|
+
# artifacts matching the search to change
|
173
|
+
#
|
174
|
+
# Consider using `clean --archive` instead
|
175
|
+
def archive
|
176
|
+
dates = parse_date_options
|
177
|
+
filter = load_artifact_filter
|
178
|
+
archive_to = parse_archive_option
|
179
|
+
if archive_to.nil?
|
180
|
+
STDERR.puts "Missing required `--archive-to` option specifying a a valid, existing directory under which to store archived artifacts"
|
181
|
+
exit Sysexits::EX_USAGE
|
182
|
+
end
|
183
|
+
|
184
|
+
report = {
|
185
|
+
archived: {
|
186
|
+
artifact_count: 0,
|
187
|
+
bytes: 0
|
188
|
+
},
|
189
|
+
skipped: {
|
190
|
+
artifact_count: 0,
|
191
|
+
bytes: 0
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
@controller.with_discovered_artifacts(from: dates[:from], to: dates[:to], repos: options.repos, threads: options.threads) do |artifact|
|
196
|
+
if artifact_meets_criteria(artifact, dates, filter)
|
197
|
+
if options.dry_run?
|
198
|
+
STDERR.puts "Would archive #{artifact} to #{archive_to}"
|
199
|
+
else
|
200
|
+
@controller.archive_artifact artifact, archive_to
|
201
|
+
end
|
202
|
+
|
203
|
+
report[:archived][:artifact_count] += 1
|
204
|
+
report[:archived][:bytes] += artifact.size
|
205
|
+
else
|
206
|
+
STDERR.puts "[DEBUG] Skipped #{artifact.inspect} because it did not meet the criteria" if options.verbose?
|
207
|
+
report[:skipped][:artifact_count] += 1
|
208
|
+
report[:skipped][:bytes] += artifact.size
|
209
|
+
end
|
210
|
+
end
|
211
|
+
report.each do |key,values|
|
212
|
+
puts "#{key} #{values[:artifact_count]} artifacts totaling #{Util.filesize values[:bytes]}"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
desc "clean", "Delete artifacts meeting specific criteria"
|
217
|
+
option :dry_run, :aliases => '-n', :type => :boolean, :desc => "Do not actually delete anything, only show what actions would have ben taken"
|
218
|
+
option :repos, :type => :array, :desc => "List of repos to clean; will delete from all repos if omitted"
|
219
|
+
option :from, :type => :string, :default => (Time.now - 2*365*24*3600).to_s, :desc => "Earliest date to include in search; defaults to 2 years ago"
|
220
|
+
option :created_before, :type => :string, :desc => "Delete artifacts with a created date earlier than the provided value"
|
221
|
+
option :modified_before, :type => :string, :desc => "Delete artifacts with a last modified date earlier than the provided value"
|
222
|
+
option :downloaded_before, :type => :string, :desc => "Delete artifacts with a last downloaded date earlier than the provided value"
|
223
|
+
option :last_used_before, :type => :string, :desc => "Delete artifacts which were created, last modified and last downloaded before the provided date"
|
224
|
+
option :threads, :type => :numeric, :default => 4, :desc => "Number of threads to use for fetching artifact info"
|
225
|
+
option :archive_to, :type => :string, :desc => "Save artifacts to the provided path before deletion"
|
226
|
+
option :filter, :type => :string, :desc => "Specify a YAML file containing filter rules to use"
|
227
|
+
##
|
228
|
+
# Delete artifacts meeting specific criteria
|
229
|
+
#
|
230
|
+
# Clean up an Artifactory instance by deleting old, unused artifacts which meet given criteria
|
231
|
+
#
|
232
|
+
# This is a CLI interface to Artifactory::Cleaner's primary function: deleting artifacts which have not been used
|
233
|
+
# in a long time (or which meet other criteria, determined by the powerful regex-based filters)
|
234
|
+
def clean
|
235
|
+
dates = parse_date_options
|
236
|
+
filter = load_artifact_filter
|
237
|
+
archive_to = parse_archive_option
|
238
|
+
|
239
|
+
# Ready to locate and delete artifacts
|
240
|
+
report = {
|
241
|
+
deleted: {
|
242
|
+
artifact_count: 0,
|
243
|
+
bytes: 0
|
244
|
+
},
|
245
|
+
archived: {
|
246
|
+
artifact_count: 0,
|
247
|
+
bytes: 0
|
248
|
+
},
|
249
|
+
skipped: {
|
250
|
+
artifact_count: 0,
|
251
|
+
bytes: 0
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
STDERR.puts "[DEBUG] controller.bucketize_artifacts from #{dates[:from]} to #{dates[:to]} repos #{options.repos}" if options.verbose?
|
256
|
+
@controller.with_discovered_artifacts(from: dates[:from], to: dates[:to], repos: options.repos, threads: options.threads) do |artifact|
|
257
|
+
if artifact_meets_criteria(artifact, dates, filter)
|
258
|
+
if archive_to
|
259
|
+
if options.dry_run?
|
260
|
+
STDERR.puts "Would archive #{artifact} to #{archive_to}"
|
261
|
+
else
|
262
|
+
@controller.archive_artifact artifact, archive_to
|
263
|
+
end
|
264
|
+
|
265
|
+
report[:archived][:artifact_count] += 1
|
266
|
+
report[:archived][:bytes] += artifact.size
|
267
|
+
end
|
268
|
+
|
269
|
+
if options.dry_run?
|
270
|
+
STDERR.puts "Would delete #{artifact}"
|
271
|
+
else
|
272
|
+
@controller.delete_artifact artifact
|
273
|
+
end
|
274
|
+
|
275
|
+
report[:deleted][:artifact_count] += 1
|
276
|
+
report[:deleted][:bytes] += artifact.size
|
277
|
+
else
|
278
|
+
STDERR.puts "[DEBUG] Skipped #{artifact.inspect} because it did not meet the criteria" if options.verbose?
|
279
|
+
report[:skipped][:artifact_count] += 1
|
280
|
+
report[:skipped][:bytes] += artifact.size
|
281
|
+
end
|
282
|
+
end
|
283
|
+
report.each do |key,values|
|
284
|
+
puts "#{key} #{values[:artifact_count]} artifacts totaling #{Util.filesize values[:bytes]}"
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
private
|
289
|
+
|
290
|
+
##
|
291
|
+
# Loads the Artifactory configuration from a YAML file
|
292
|
+
def load_conf_file(path)
|
293
|
+
config = YAML.load_file path
|
294
|
+
config.each do |key, val|
|
295
|
+
@config[key] = val
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# Initialize our Artifactory::Cleaner::Controller
|
301
|
+
def create_controller
|
302
|
+
@controller = Artifactory::Cleaner::Controller.new(@artifactory_config)
|
303
|
+
@controller.verbose = true if options.verbose?
|
304
|
+
end
|
305
|
+
|
306
|
+
##
|
307
|
+
# return Ruby Time objects formed from CLI switches `--to`, `--from`, `--ctreated-before` etc
|
308
|
+
def parse_date_options
|
309
|
+
dates = {}
|
310
|
+
dates[:from] = Time.parse(options.from) if options.from
|
311
|
+
dates[:created_before] = Time.parse(options.created_before) if options.created_before
|
312
|
+
dates[:modified_before] = Time.parse(options.modified_before) if options.modified_before
|
313
|
+
dates[:last_used_before] = Time.parse(options.last_used_before) if options.last_used_before
|
314
|
+
dates[:to] = [dates[:created_before], dates[:modified_before], dates[:downloaded_before], dates[:last_used_before]].compact.sort.first
|
315
|
+
|
316
|
+
if dates[:to].nil?
|
317
|
+
STDERR.puts "At least one end date for search must be provided (--created-before, --modified-before, --downloaded-before or --last-used-before)"
|
318
|
+
exit Sysexits::EX_USAGE
|
319
|
+
end
|
320
|
+
|
321
|
+
dates
|
322
|
+
end
|
323
|
+
|
324
|
+
##
|
325
|
+
# Parse and validate value for the `--archive-to` CLI switch, ensuring it points to a valid, writable directory
|
326
|
+
def parse_archive_option
|
327
|
+
archive_to = options.archive_to
|
328
|
+
if archive_to
|
329
|
+
unless File.directory? archive_to
|
330
|
+
STDERR.puts "#{archive_to} is not a directory. `--archive-to` expects a valid, existing directory under which to store archived artifacts"
|
331
|
+
exit Sysexits::EX_USAGE
|
332
|
+
end
|
333
|
+
archive_to = File.realpath(archive_to)
|
334
|
+
unless File.directory? archive_to and File.writable? archive_to
|
335
|
+
STDERR.puts "Unable to write to directory #{archive_to} -- check permissions"
|
336
|
+
exit Sysexits::EX_CANTCREAT
|
337
|
+
end
|
338
|
+
end
|
339
|
+
archive_to
|
340
|
+
end
|
341
|
+
|
342
|
+
##
|
343
|
+
# Load Artifactory::Cleaner::ArtifactFilter objects from a YAML file
|
344
|
+
def load_artifact_filter
|
345
|
+
filter = ArtifactFilter.new
|
346
|
+
if options.filter
|
347
|
+
unless File.exist? options.filter and File.readable? options.filter
|
348
|
+
STDERR.puts "Unable to read specified filter file #{options.filter}"
|
349
|
+
exit Sysexits::EX_USAGE
|
350
|
+
end
|
351
|
+
rules = YAML.load_file options.filter
|
352
|
+
rules.each {|rule| filter << rule}
|
353
|
+
end
|
354
|
+
filter
|
355
|
+
end
|
356
|
+
|
357
|
+
##
|
358
|
+
# Check if a given artifact meets our CLI search criteria and filters
|
359
|
+
def artifact_meets_criteria(artifact, dates, filter)
|
360
|
+
(dates.has_key?(:created_before) ? artifact.created < dates[:created_before] : true) and
|
361
|
+
(dates.has_key?(:modified_before) ? artifact.last_modified < dates[:modified_before] : true) and
|
362
|
+
(dates.has_key?(:last_used_before) ? artifact.latest_date < dates[:last_used_before] : true) and
|
363
|
+
(filter.action_for(artifact) == :include)
|
364
|
+
end
|
365
|
+
|
366
|
+
##
|
367
|
+
#
|
368
|
+
def repo_cols(repo, include_cols)
|
369
|
+
include_cols.map do |col|
|
370
|
+
repo.send(col.method).to_s
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
##
|
375
|
+
# Helper method for generating CLI terminal-friendly tables of output
|
376
|
+
def get_repo_cols(repo_kinds)
|
377
|
+
if options.details?
|
378
|
+
selected_cols =
|
379
|
+
if options.output.nil?
|
380
|
+
Artifactory::Cleaner::CLI.default_repo_table_cols
|
381
|
+
else
|
382
|
+
options.output.split(',').map &:to_sym
|
383
|
+
end
|
384
|
+
@repo_table_cols.select do |col|
|
385
|
+
(col.only.nil? or repo_kinds.include? col.only) and (selected_cols.include? col.method)
|
386
|
+
end
|
387
|
+
else
|
388
|
+
@repo_table_cols.select {|col| col.method == :key}
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
##
|
393
|
+
# CLI helper method for printing details of discovered repositories to a terminal
|
394
|
+
def print_repo_list(repo_info_table, include_cols)
|
395
|
+
if options.no_headers || !options.details
|
396
|
+
repo_info_table.each {|row| puts row.join("\t")}
|
397
|
+
else
|
398
|
+
headers = include_cols.map &:heading
|
399
|
+
widths = headers.map {|h| h.length + 1}
|
400
|
+
repo_info_table.each do |row|
|
401
|
+
row.each_with_index { |val, index| widths[index] = [widths[index], val.length + 1].max }
|
402
|
+
end
|
403
|
+
total_width = widths.reduce(0) {|s,v| s+v}
|
404
|
+
headers.each_with_index { |h, i| print h.ljust(widths[i], ' ') }
|
405
|
+
puts ""
|
406
|
+
puts '-'.ljust(total_width, '-')
|
407
|
+
repo_info_table.each do |row|
|
408
|
+
row.each_with_index { |v, i| print v.ljust(widths[i], ' ') }
|
409
|
+
puts ""
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
@@ -0,0 +1,466 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
module Artifactory
|
4
|
+
module Cleaner
|
5
|
+
##
|
6
|
+
# Artifactory Cleaner Logic Controller
|
7
|
+
#
|
8
|
+
# The Artifactory::Cleaner::Controller class provides logic central to Artifactory Cleaner.
|
9
|
+
# Artifactory::Cleaner::Controller manages the Artifactory API client, performs searches, discovers artifacts, and
|
10
|
+
# more. It is capable of executing tasks in a multi-threaded fashion, making multiple requests to the Artifactory
|
11
|
+
# server in parallel.
|
12
|
+
class Controller
|
13
|
+
##
|
14
|
+
# Struct to contain the processing queues used internally within the controller
|
15
|
+
# ---
|
16
|
+
# The controller contains two {Queues}[https://ruby-doc.org/core-2.6/Queue.html] which are used for multi-threaded
|
17
|
+
# processing of artifacts. The :incoming queue is fed tasks to be done, which Artifactory::Cleaner::DiscoveryWorker
|
18
|
+
# instances pop from, process, and push the results back into the :outgoing queue. The Artifactory::Cleaner::Controller
|
19
|
+
# will then pop from the outgoing queue and send the results back to the caller
|
20
|
+
ProcessingQueues = Struct.new(:incoming, :outgoing)
|
21
|
+
|
22
|
+
##
|
23
|
+
# Initialize and configure a new Artifactory::Cleaner::Controller
|
24
|
+
# Params:
|
25
|
+
# +artifactory_config+:: Hash of configuration for the Artifactory client. Used as a splat for a call to Artifactory::Client.new
|
26
|
+
def initialize(artifactory_config)
|
27
|
+
@artifactory_client = client = Artifactory::Client.new(**artifactory_config)
|
28
|
+
@verbose = false
|
29
|
+
initialize_queues
|
30
|
+
@workers = []
|
31
|
+
@num_workers = 6
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Is verbose output enabled? If so, the controller will print debugging and status information to STDERR
|
36
|
+
def verbose?
|
37
|
+
@verbose
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Enable or disable verbose mode (see Controller#verbose?)
|
42
|
+
# When verbose mode is enabled, the controller will print debugging and status information to STDERR
|
43
|
+
def verbose=(val)
|
44
|
+
@verbose = !!val
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# Return an ordered structure of repositories from the Artifactory server.
|
49
|
+
#
|
50
|
+
# This method will query Artifactory and fetch information about all available repositories. The result returned
|
51
|
+
# is a Hash with three keys, one for each repo type: `:local`, `:remote` and `:virtual`
|
52
|
+
# Under each of these keys is a hash mapping repo keys to their Artifactory::Resource::Repository objects
|
53
|
+
#
|
54
|
+
# This method may raise network errors from the underlying Artifactory client
|
55
|
+
#
|
56
|
+
# This method is not multi-threaded
|
57
|
+
def discover_repos
|
58
|
+
timing = {}
|
59
|
+
@repos = {
|
60
|
+
local: {},
|
61
|
+
remote: {},
|
62
|
+
virtual: {},
|
63
|
+
}
|
64
|
+
i = 0
|
65
|
+
timing[:loop] = Benchmark.measure do
|
66
|
+
@artifactory_client.repository_all.each do |repo|
|
67
|
+
debuglog "[DEBUG] Found #{repo.package_type} repo: #{repo.key}"
|
68
|
+
if repo.rclass == 'remote' && repo.url
|
69
|
+
debuglog " +-> repo #{repo.key} is a mirror of remote at #{repo.url}"
|
70
|
+
@repos[:remote][repo.key] = repo
|
71
|
+
elsif repo.rclass == 'virtual' && repo.repositories
|
72
|
+
debuglog " +-> repo #{repo.key} is a virtual repo containing: #{repo.repositories.join ', '}"
|
73
|
+
@repos[:remote][repo.key] = repo
|
74
|
+
else
|
75
|
+
@repos[:local][repo.key] = repo
|
76
|
+
end
|
77
|
+
i += 1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
debuglog("[DEBUG][Perfdata] Fetched #{i} repos; timing: #{timing[:loop]}")
|
81
|
+
@repos
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Given a list of Artifacts, fetch information about them and return a list of Artifactory::Cleaner::DiscoveredArtifact instances
|
86
|
+
#
|
87
|
+
# This is a helper function for #artifact_usage_search
|
88
|
+
#
|
89
|
+
# TODO: Document format of the `artifact_list` parameter
|
90
|
+
#
|
91
|
+
# This method may throw network errors from the underlying Artifactory client
|
92
|
+
#
|
93
|
+
# This method is multi-threaded and will spawn workers in order to make multiple concurrent HTTP connections to
|
94
|
+
# the Artifactory API. The number of threads can be tuned with the +`threads`+ parameter. Be careful not to
|
95
|
+
# cause excessive load on the Artifactory API!
|
96
|
+
def discover_artifacts_from_search(artifact_list, threads: 4)
|
97
|
+
result = []
|
98
|
+
timing = {}
|
99
|
+
#kill_threads
|
100
|
+
@num_workers = threads
|
101
|
+
timing[:enqueue] = Benchmark.measure do
|
102
|
+
artifact_list.each {|a| queue_discovery_of_artifact a}
|
103
|
+
end
|
104
|
+
|
105
|
+
timing[:dequeue] = Benchmark.measure do
|
106
|
+
until @discovery_queues.incoming.empty? and @discovery_queues.outgoing.empty? and not @workers.any? &:working?
|
107
|
+
begin
|
108
|
+
item = @discovery_queues.outgoing.pop
|
109
|
+
if item.kind_of? Artifactory::Resource::Artifact
|
110
|
+
result << item
|
111
|
+
#debuglog "[DEBUG] Discovered #{item} from a child thread"
|
112
|
+
elsif item.kind_of? Error
|
113
|
+
STDERR.puts "[ERROR] Error from artifact fetch: #{item}"
|
114
|
+
STDERR.puts item.full_message
|
115
|
+
STDERR.puts "Caused by #{item.cause.full_message}" if item.cause
|
116
|
+
elsif !artifact.nil?
|
117
|
+
STDERR.puts "[ERROR] Got #{item} back from the discovery queue, expected an Artifactory::Resource::Artifact"
|
118
|
+
end
|
119
|
+
rescue => processing_ex
|
120
|
+
STDERR.puts "[ERROR] Caught an exception when processing from the outgoing discovery queue: #{processing_ex}"
|
121
|
+
STDERR.puts processing_ex.full_message
|
122
|
+
STDERR.puts "Caused by #{processing_ex.cause.full_message}" if processing_ex.cause
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
begin
|
128
|
+
kill_threads
|
129
|
+
rescue => ex
|
130
|
+
STDERR.puts "[ERROR] Caught an exception when killing threads: #{ex}"
|
131
|
+
STDERR.puts ex.full_message
|
132
|
+
STDERR.puts "Caused by #{ex.cause.full_message}" if ex.cause
|
133
|
+
end
|
134
|
+
|
135
|
+
debuglog("[DEBUG][Perfdata] Enqueue URLs for workers to discover: #{timing[:enqueue]}")
|
136
|
+
debuglog("[DEBUG][Perfdata] Dequeue found Artifacts from workers: #{timing[:dequeue]}")
|
137
|
+
total_time = timing.values.reduce(0) {|s,t| s + t.real}
|
138
|
+
debuglog("[DEBUG] #{result.length} artifacts fetched in #{total_time.round 2} seconds")
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Search for an artifact by its usage
|
144
|
+
#
|
145
|
+
# @example Search for all repositories with the given usage statistics
|
146
|
+
# Artifact.usage_search(
|
147
|
+
# notUsedSince: 1388534400000,
|
148
|
+
# createdBefore: 1388534400000,
|
149
|
+
# )
|
150
|
+
#
|
151
|
+
# @example Search for all artifacts with the given usage statistics in a repo
|
152
|
+
# Artifact.usage_search(
|
153
|
+
# notUsedSince: 1388534400000,
|
154
|
+
# createdBefore: 1388534400000,
|
155
|
+
# repos: 'libs-release-local',
|
156
|
+
# )
|
157
|
+
#
|
158
|
+
# @param [Hash] options
|
159
|
+
# the list of options to search with
|
160
|
+
#
|
161
|
+
# @option options [Artifactory::Client] :client
|
162
|
+
# the client object to make the request with
|
163
|
+
# @option options [Long] :notUsedSince
|
164
|
+
# the last downloaded cutoff date of the artifact to search for (millis since epoch)
|
165
|
+
# @option options [Long] :createdBefore
|
166
|
+
# the creation cutoff date of the artifact to search for (millis since epoch)
|
167
|
+
# @option options [String, Array<String>] :repos
|
168
|
+
# the list of repos to search
|
169
|
+
#
|
170
|
+
# @return [Array<Resource::Artifact>]
|
171
|
+
# a list of artifacts that match the query
|
172
|
+
#
|
173
|
+
def artifact_usage_search(from: nil, to: nil, repos: nil, threads: 4)
|
174
|
+
to = Time.now if to.nil?
|
175
|
+
|
176
|
+
params = {
|
177
|
+
dateFields: 'created,lastModified,lastDownloaded',
|
178
|
+
from: from.is_a?(Time) ? from.to_i * 1000 : from.to_i,
|
179
|
+
to: to.is_a?(Time) ? to.to_i * 1000 : to.to_i
|
180
|
+
}
|
181
|
+
repos = repos.compact.join(",") unless repos.nil?
|
182
|
+
params[:repos] = repos unless repos.nil?
|
183
|
+
|
184
|
+
result = nil
|
185
|
+
|
186
|
+
debuglog("[DEBUG] Making Artifactory request /api/search/dates for #{params.inspect}")
|
187
|
+
timing = {}
|
188
|
+
timing[:search] = Benchmark.measure do
|
189
|
+
begin
|
190
|
+
result = @artifactory_client.get("/api/search/dates", params)
|
191
|
+
rescue Artifactory::Error::HTTPError => err
|
192
|
+
if err.code == 404
|
193
|
+
debuglog " HTTP 404 Not Found fetching: /api/search/dates -- assuming no assets for this date range"
|
194
|
+
result = []
|
195
|
+
#Pry::rescued(err) if defined?(Pry::rescue)
|
196
|
+
else
|
197
|
+
STDERR.puts "HTTP Error while performing an artifact usage search: #{err}"
|
198
|
+
STDERR.puts err.full_message
|
199
|
+
STDERR.puts "Parameters were: #{params.inspect}"
|
200
|
+
STDERR.puts "Caused by #{err.cause.full_message}" if err.cause
|
201
|
+
Pry::rescued(err) if defined?(Pry::rescue)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
debuglog("[DEBUG] Got #{result["results"].length} results from search in #{timing[:search].real} seconds") unless result.nil? or result.empty?
|
206
|
+
timing[:fetch] = Benchmark.measure do
|
207
|
+
if threads > 1
|
208
|
+
unless result.nil? or result.empty?
|
209
|
+
result = discover_artifacts_from_search(result["results"], threads: threads)
|
210
|
+
end
|
211
|
+
else
|
212
|
+
unless result.nil? or result.empty?
|
213
|
+
result = result["results"].map do |artifact|
|
214
|
+
a = nil
|
215
|
+
retries = 10
|
216
|
+
while a.nil? and retries > 0
|
217
|
+
begin
|
218
|
+
retries -= 1
|
219
|
+
a = Artifactory::Cleaner::DiscoveredArtifact.from_url(artifact["uri"], client: @artifactory_client)
|
220
|
+
a.last_downloaded = Time.parse(artifact["lastDownloaded"]) unless artifact["lastDownloaded"].to_s.empty?
|
221
|
+
rescue Net::OpenTimeout, Artifactory::Error::ConnectionError => err
|
222
|
+
STDERR.puts "[WARN] Connection Failure attempting to reach Artifactory API: #{err}"
|
223
|
+
debuglog " Parameters were: #{params.inspect}"
|
224
|
+
debuglog " Caused by #{err.cause.full_message}" if err.cause
|
225
|
+
STDERR.puts " Retrying in 10 seconds" if retries
|
226
|
+
sleep 10
|
227
|
+
rescue Artifactory::Error::HTTPError => err
|
228
|
+
if err.code == 404
|
229
|
+
STDERR.puts "[WARN] HTTP 404 Not Found fetching: #{artifact["uri"]}"
|
230
|
+
retries = 0
|
231
|
+
else
|
232
|
+
retries = min(retries, 1)
|
233
|
+
STDERR.puts "[ERROR] HTTP Error while fetching an artifact from a usage search: #{err}"
|
234
|
+
debuglog err.full_message
|
235
|
+
debuglog " Artifact was: #{artifact.inspect}"
|
236
|
+
debuglog " Parameters were: #{params.inspect}"
|
237
|
+
debuglog " Caused by #{err.cause.full_message}" if err.cause
|
238
|
+
Pry::rescued(err) if defined?(Pry::rescue)
|
239
|
+
STDERR.puts " Will retry download once" if retries
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
a
|
244
|
+
end
|
245
|
+
end
|
246
|
+
result.compact!
|
247
|
+
end
|
248
|
+
end
|
249
|
+
debuglog("[DEBUG][Perfdata] Artifactory request /api/search/dates timing: #{timing[:search]}")
|
250
|
+
debuglog("[DEBUG][Perfdata] Fetching artifacts timing: #{timing[:fetch]}")
|
251
|
+
total_time = timing.values.reduce(0) {|s,t| s + t.real}
|
252
|
+
debuglog("[DEBUG] #{result.length} artifacts fetched in #{total_time.round 2} seconds")
|
253
|
+
result
|
254
|
+
end
|
255
|
+
|
256
|
+
##
|
257
|
+
# Iterator method for an artifact search
|
258
|
+
#
|
259
|
+
# the `with_discovered_artifacts` method is used to iterate over artifacts from a search which potentially covers
|
260
|
+
# a large period of time. This method will break the period up into small chunks of time defined by the
|
261
|
+
# `increment` argument (defaulting to 30 days) and will perform multiple searches to avoid large searches which
|
262
|
+
# may time out or overload the Artifactory server.
|
263
|
+
#
|
264
|
+
# Pass a block and the block will be called with every Artifactory::Cleaner::DiscoveredArtifact that is found
|
265
|
+
#
|
266
|
+
# This method is not mult-threaded however it calls artifact_usage_search which is multi-threaded; number of
|
267
|
+
# threads is controlled by the `threads` argument
|
268
|
+
#
|
269
|
+
# This method calls artifact_usage_search which may raise network exceptions
|
270
|
+
#
|
271
|
+
# Params:
|
272
|
+
# +from+:: Time instance for the start date of the search
|
273
|
+
# +to+:: Time instance for the end date of the search; defaults to Time.now
|
274
|
+
# +repos+:: Optional array of repository names to search within; searches all repositories if omitted
|
275
|
+
# +increment+:: Integer number of seconds to chunk the search period into, defaults to 30 days
|
276
|
+
# +threads+:: Number of threads to use to fetch artifacts; defayult is 4 (passed to artifact_usage_search)
|
277
|
+
def with_discovered_artifacts(from: nil, to: nil, repos: nil, increment: 30 * 24 * 3600, threads: 4)
|
278
|
+
chunk_end = to || Time.now
|
279
|
+
while chunk_end > from
|
280
|
+
chunk_start = chunk_end - increment
|
281
|
+
chunk_start = from if chunk_start < from
|
282
|
+
artifact_usage_search(from: chunk_start, to: chunk_end, repos: repos, threads: threads).each do |pkg|
|
283
|
+
yield pkg
|
284
|
+
end
|
285
|
+
chunk_end = chunk_start
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def bucketize_artifacts(from: nil, to: nil, increment: 30 * 24 * 3600, repos: nil, buckets: nil, threads: 4)
|
290
|
+
buckets = ArtifactBucketCollection.new unless buckets.is_a? ArtifactBucketCollection
|
291
|
+
with_discovered_artifacts(from: from, to: to, repos: repos, increment: increment, threads: threads) do |artifact|
|
292
|
+
buckets << artifact
|
293
|
+
end
|
294
|
+
buckets
|
295
|
+
end
|
296
|
+
|
297
|
+
##
|
298
|
+
# Given a Artifactory::Cleaner::ArtifactBucketCollection, return a String summarizing the contents
|
299
|
+
#
|
300
|
+
# TODO: This really should be a method on Artifactory::Cleaner::ArtifactBucketCollection
|
301
|
+
def bucketized_artifact_report(buckets)
|
302
|
+
total_size = 0
|
303
|
+
total_count = 0
|
304
|
+
lines = buckets.map do |bucket|
|
305
|
+
total_size += bucket.filesize
|
306
|
+
total_count += bucket.length
|
307
|
+
"#{bucket.length} artifacts between #{bucket.min} and #{bucket.max} days, totaling #{Artifactory::Cleaner::Util::filesize bucket.filesize}"
|
308
|
+
end
|
309
|
+
lines << "Total: #{Artifactory::Cleaner::Util::filesize total_size} across #{total_count} artifacts"
|
310
|
+
end
|
311
|
+
|
312
|
+
##
|
313
|
+
# Return a YAML representation of a module Artifactory::Cleaner::DiscoveredArtifact
|
314
|
+
#
|
315
|
+
# Provide a Artifactory::Cleaner::DiscoveredArtifact and this method will return a String containing a YAML
|
316
|
+
# representation of the properties of the DiscoveredArtifact. If the `indent` parameter is provided, then a YAML
|
317
|
+
# fragment will be returned, indented by `indent` spaces. This allows for "streaming" a list of Artifact YAML to
|
318
|
+
# an IOStream
|
319
|
+
def yaml_format(artifact, indent = 0)
|
320
|
+
properties = [:uri, :last_downloaded, :repo, :created, :last_modified, :last_updated, :download_uri, :mime_type, :size, :checksums ]
|
321
|
+
result = YAML.dump(properties.each_with_object({}) {|prop,export| export[prop] = artifact.send(prop) })
|
322
|
+
if indent
|
323
|
+
i = 0
|
324
|
+
result.each_line.reduce('') do |str,line|
|
325
|
+
if (i += 1) > 2
|
326
|
+
str + (' ' * indent) + line
|
327
|
+
elsif i == 2
|
328
|
+
str + line
|
329
|
+
else
|
330
|
+
str
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
##
|
337
|
+
# Download a copy of an artifact to the local filesystem prior to deletion
|
338
|
+
#
|
339
|
+
# Given an Artifactory::Resource::Artifact `artifact`, download the artifact to the local filesystem directory
|
340
|
+
# specified by the `path` param
|
341
|
+
#
|
342
|
+
# **Note:** Downloading an artifact will update the artifact's last_downloaded date so it may no longer match the
|
343
|
+
# same search criteria it originally die (if last_downlaoded was used to discover this artifact)
|
344
|
+
#
|
345
|
+
# This method is meant to be used prior to calling `delete_artifact`
|
346
|
+
def archive_artifact(artifact, path)
|
347
|
+
debuglog "[DEBUG] downloading #{artifact} (#{artifact.uri}) to #{path}"
|
348
|
+
timing = Benchmark.measure do
|
349
|
+
artifact.download(path)
|
350
|
+
end
|
351
|
+
debuglog "[DEBUG] #{artifact.uri} #{Util.filesize artifact.size} downloaded in #{timing.real.round(2)} seconds (#{Util.filesize(artifact.size/timing.real)})/s"
|
352
|
+
end
|
353
|
+
|
354
|
+
##
|
355
|
+
# Delete an artifact from the Artifactory server
|
356
|
+
#
|
357
|
+
# Given an Artifactory::Resource::Artifact `artifact`, delete it from the Artifactory server. **This is a
|
358
|
+
# destructive operation -- use with caution!**
|
359
|
+
#
|
360
|
+
# Consider using `archive_artifact` first to save artifacts locally
|
361
|
+
#
|
362
|
+
# This function writes to the remote Artifactory server (specifically it makes a delete call)
|
363
|
+
def delete_artifact(artifact)
|
364
|
+
debuglog "[DEBUG] DELETE Artifact #{artifact} at #{artifact.uri}!"
|
365
|
+
artifact.delete
|
366
|
+
end
|
367
|
+
|
368
|
+
##
|
369
|
+
# Deprecated, do not use
|
370
|
+
def catagorize_old_assets(days)
|
371
|
+
buckets = {
|
372
|
+
730 => {count: 0, size: 0},
|
373
|
+
365 => {count: 0, size: 0},
|
374
|
+
180 => {count: 0, size: 0},
|
375
|
+
90 => {count: 0, size: 0},
|
376
|
+
30 => {count: 0, size: 0},
|
377
|
+
}
|
378
|
+
discover_repos
|
379
|
+
@repos[:local].each_pair do |id,repo|
|
380
|
+
begin
|
381
|
+
pkgs = 0
|
382
|
+
purgable = 0
|
383
|
+
timings = Benchmark.bm(12) do |bm|
|
384
|
+
debuglog "Searching Repo #{id}:"
|
385
|
+
old_packages = nil
|
386
|
+
bm.report('api call') {
|
387
|
+
old_packages = @artifactory_client.artifact_usage_search(
|
388
|
+
notUsedSince: (Time.now.to_i - 24 * 3600 * days) * 1000,
|
389
|
+
createdBefore: (Time.now.to_i - 24 * 3600 * days) * 1000,
|
390
|
+
repos: id
|
391
|
+
)
|
392
|
+
}
|
393
|
+
debuglog " Artifactory search returned #{old_packages.length} assets older than #{days}..."
|
394
|
+
bm.report('loop') { old_packages.each_with_index do |pkg,i|
|
395
|
+
pkgs += 1
|
396
|
+
uri = URI(pkg.uri)
|
397
|
+
purgable += pkg.size
|
398
|
+
# Calculate the age of this package in days and increment the bucket it belongs in
|
399
|
+
age = (Time.now - pkg.last_modified)/(3600*24)
|
400
|
+
if (bucket = buckets.keys.find {|v| age >= v })
|
401
|
+
buckets[bucket][:count] += 1
|
402
|
+
buckets[bucket][:size] += pkg.size
|
403
|
+
end
|
404
|
+
debuglog " ##{i}: #{File.basename(uri.path)} #{Util.filesize pkg.size} Created #{pkg.created} Modified #{pkg.last_modified}"
|
405
|
+
end }
|
406
|
+
end
|
407
|
+
debuglog "Found #{pkgs} assets from #{id} older than #{days} days totaling #{Util.filesize purgable} in #{timings.reduce {|sum, t| sum + t.real}} seconds"
|
408
|
+
rescue => ex
|
409
|
+
STDERR.puts "Caught an exception trying to handle repo #{id}: #{ex}"
|
410
|
+
STDERR.puts ex.full_message
|
411
|
+
STDERR.puts "Caused by #{ex.cause.full_message}" if ex.cause
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
buckets.each_pair do |age,bucket|
|
416
|
+
debuglog "#{bucket[:count]} packages older than #{age} days, totaling #{Util.filesize bucket[:size]}"
|
417
|
+
end
|
418
|
+
|
419
|
+
buckets
|
420
|
+
end
|
421
|
+
|
422
|
+
##################################################################################################################
|
423
|
+
|
424
|
+
private
|
425
|
+
|
426
|
+
##
|
427
|
+
# debug/verbose logging
|
428
|
+
def debuglog(msg)
|
429
|
+
STDERR.puts msg if @verbose
|
430
|
+
end
|
431
|
+
|
432
|
+
##
|
433
|
+
# Initialize empty artifact discovery queues
|
434
|
+
def initialize_queues
|
435
|
+
@discovery_queues = ProcessingQueues.new
|
436
|
+
@discovery_queues.incoming = Queue.new
|
437
|
+
@discovery_queues.outgoing = Queue.new
|
438
|
+
end
|
439
|
+
|
440
|
+
##
|
441
|
+
# make sure we have the desired number of worker threads
|
442
|
+
def spawn_threads
|
443
|
+
while @workers.length < @num_workers
|
444
|
+
@workers << DiscoveryWorker.new(@discovery_queues, @artifactory_client).start
|
445
|
+
debuglog "[DEBUG] Spawned #{@workers.last} to process discovery calls"
|
446
|
+
end
|
447
|
+
end
|
448
|
+
|
449
|
+
##
|
450
|
+
# given artifact data, add it to the queue for processing and make sure we have workers to process it
|
451
|
+
def queue_discovery_of_artifact(artifact_data)
|
452
|
+
@discovery_queues.incoming.push(artifact_data)
|
453
|
+
#debuglog "[DEBUG] Queued #{artifact_data['uri']} for discovery"
|
454
|
+
spawn_threads
|
455
|
+
end
|
456
|
+
|
457
|
+
##
|
458
|
+
# Forcibly terminate all threads
|
459
|
+
# TODO: add a graceful terminate method
|
460
|
+
def kill_threads
|
461
|
+
@workers.each &:kill
|
462
|
+
@workers = []
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|