|
1 | 1 | require 'date' |
2 | 2 | require 'octokit' |
| 3 | +require 'faraday' |
3 | 4 | require 'sinatra' |
4 | 5 | require 'sinatra/json' |
5 | 6 |
|
|
8 | 9 | set :server_settings, timeout: 60 |
9 | 10 | end |
10 | 11 |
|
11 | | -def has_permatag?(version, permatags) |
12 | | - (version['metadata']['container']['tags'] & permatags).any? |
| 12 | +stack = Faraday::RackBuilder.new do |builder| |
| 13 | + builder.use Octokit::Middleware::FollowRedirects |
| 14 | + builder.use Octokit::Response::RaiseError |
| 15 | + builder.use Octokit::Response::FeedParser |
| 16 | + builder.response :logger, nil, { headers: true, bodies: true, errors: true } do |logger| |
| 17 | + logger.filter(/(Authorization: "(token|Bearer) )(\w+)/, '\1[REMOVED]') |
| 18 | + end |
| 19 | + builder.adapter Faraday.default_adapter |
13 | 20 | end |
| 21 | +Octokit.middleware = stack |
| 22 | + |
| 23 | +module Logging |
| 24 | + def logger |
| 25 | + Logging.logger |
| 26 | + end |
14 | 27 |
|
15 | | -def younger_than?(version, days_old) |
16 | | - cutoff = Time.now - 60*60*24*days_old |
17 | | - version['created_at'] > cutoff |
| 28 | + def self.logger |
| 29 | + @logger ||= Logger.new(STDOUT) |
| 30 | + end |
18 | 31 | end |
19 | 32 |
|
20 | | -github = Octokit::Client.new(per_page: 100, auto_paginate: true) |
| 33 | +class RegistryPruner |
| 34 | + include Logging |
21 | 35 |
|
22 | | -get '/' do |
23 | | - 'Hello, world!' |
24 | | -end |
| 36 | + attr_reader :github, :org |
25 | 37 |
|
26 | | -# Returns all container packages along with their pruning status, i.e. whether they can/can't be pruned and why |
27 | | -get '/images' do |
28 | | - packages = github.get('orgs/BerkeleyLibrary/packages', {package_type: :container}) |
29 | | - logger.info "Scanning #{packages.size} packages for prunable images: #{packages.collect(&:name).sort}" |
| 38 | + def initialize(github: nil, org: 'BerkeleyLibrary') |
| 39 | + @github = github || Octokit::Client.new(per_page: 100, auto_paginate: true) |
| 40 | + @org = org |
| 41 | + @inventory = nil |
| 42 | + end |
30 | 43 |
|
31 | | - prunables = [].tap do |sofar| |
32 | | - packages.each do |pkg| |
33 | | - logger.info "Determining prunable images for #{pkg.name}" |
| 44 | + def inventory |
| 45 | + @inventory ||= [].then { refresh_inventory! } |
| 46 | + end |
34 | 47 |
|
35 | | - next unless pkg.repository |
| 48 | + def prune!(days_old = 7) |
| 49 | + cutoff = Time.now - (60*60*24 * days_old) |
36 | 50 |
|
37 | | - permatags = %w(latest edge) |
38 | | - permatags += github.branches(pkg.repository.full_name).collect(&:name) |
39 | | - permatags += github.tags(pkg.repository.full_name).collect(&:name) |
| 51 | + inventory.each do |image| |
| 52 | + if image[:created_at] > cutoff |
| 53 | + logger.debug "SKIPPING: Image #{image[:package]}/#{image[:version]} created recently: #{image[:created_at]}" |
| 54 | + next |
| 55 | + end |
| 56 | + |
| 57 | + permatags = image[:tags] & image[:repo_permatags] |
| 58 | + if permatags.any? |
| 59 | + logger.debug "SKIPPING: Image #{image[:package]}/#{image[:version]} has permatags: #{permatags.sort.join(', ')}" |
| 60 | + next |
| 61 | + end |
40 | 62 |
|
41 | | - github.get("orgs/#{pkg.owner.login}/packages/#{pkg.package_type}/#{pkg.name}/versions").each do |image| |
42 | | - if has_permatag? image, permatags |
43 | | - pruning_status = :permatagged |
44 | | - elsif younger_than? image, 7 |
45 | | - pruning_status = :recent |
| 63 | + begin |
| 64 | + logger.debug("Deleting image: #{image[:url]}") |
| 65 | + github.delete image[:url], nil |
| 66 | + rescue Octokit::BadRequest => e |
| 67 | + logger.error(e) |
| 68 | + if e.message =~ /cannot be deleted/ |
| 69 | + next |
46 | 70 | else |
47 | | - pruning_status = :prunable |
| 71 | + raise |
48 | 72 | end |
| 73 | + end |
| 74 | + end |
| 75 | + end |
49 | 76 |
|
50 | | - sofar << { |
51 | | - image: image.to_attrs, |
52 | | - pruning_status: pruning_status, |
53 | | - can_be_pruned: pruning_status == :prunable, |
54 | | - } |
| 77 | + def refresh_inventory! |
| 78 | + @inventory = [].tap do |images| |
| 79 | + github.get("orgs/#{org}/packages", { package_type: :container }).each do |pkg| |
| 80 | + next unless pkg.repository |
| 81 | + |
| 82 | + repo = pkg.repository.full_name |
| 83 | + repo_permatags = permatags_for(pkg) |
| 84 | + next_page = "orgs/#{org}/packages/container/#{pkg.name}/versions" |
| 85 | + |
| 86 | + loop do |
| 87 | + github.get(next_page).each do |image| |
| 88 | + images << { |
| 89 | + url: "orgs/#{org}/packages/container/#{pkg.name}/versions/#{image.id}", |
| 90 | + package: pkg.name, |
| 91 | + version: image.id, |
| 92 | + created_at: image['created_at'], |
| 93 | + tags: image['metadata']['container']['tags'], |
| 94 | + repo:, |
| 95 | + repo_permatags:, |
| 96 | + } |
| 97 | + end |
| 98 | + next_page = github.last_response.rels[:next]&.href |
| 99 | + break if next_page.nil? |
| 100 | + end |
55 | 101 | end |
56 | 102 | end |
57 | 103 | end |
58 | 104 |
|
59 | | - json prunables |
| 105 | + def permatags_for(pkg) |
| 106 | + %w(latest edge).tap do |permatags| |
| 107 | + permatags.concat github.branches(pkg.repository.full_name).collect(&:name) |
| 108 | + permatags.concat github.tags(pkg.repository.full_name).collect(&:name) |
| 109 | + permatags.sort! |
| 110 | + end |
| 111 | + end |
| 112 | +end |
| 113 | + |
| 114 | +get '/images' do |
| 115 | + pruner = RegistryPruner.new |
| 116 | + inventory = pruner.inventory |
| 117 | + json({ inventory: }) |
60 | 118 | end |
0 commit comments