-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdocker_image_prune.rb
executable file
·209 lines (179 loc) · 6.55 KB
/
docker_image_prune.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/usr/bin/env ruby
#
# Fucntionality to list and prune images labeled with date labels
# (in the format used by automatic Jenkins builds).
#
# - Relies on ~/.docker/config.json for auth keys.
# - Uses STDERR for user messaging so that it can be utilized more easily from bash scripts
#
# See README.md
#
require 'rest-client'
require 'json'
DEFAULT_DTR_HOSTNAME= 'dtr.cucloud.net'
DEFAULT_TAG_DATETIME_FORMAT = '%m%d%Y-%H%M%S'
DEFAULT_EXPIRATION_AGE_DAYS = 90
DEFAULT_DRY_RUN = false
DEFAULT_MINIMUM_IMAGES_TO_KEEP = 3
class DockerImagePrune
attr_accessor :dtr_hostname, :datetime_format
attr_accessor :namespace, :dry_run, :expiration_age_days
def repo_list
repos = []
STDERR.puts "Determining repos in #{dtr_repos_url}."
response = RestClient::Request.execute(
method: :get,
url: dtr_repos_url,
headers: request_headers.merge({params: {start: 0, limit: 9999}})
)
if (response.code == 200)
# puts response.body
j = JSON.parse(response.body)
j["repositories"].each { | repo | repos << repo["name"] }
end
return repos
end
def delete_tags_for_namespace
result = true
repos = repo_list
repos.each do | repo |
STDERR.puts "Processing repository: #{repo}"
expired_tags = expired_tags_for_repo(repo)
result = delete_tags_for_repo(repo, expired_tags) && result
end
return result
end
def initialize(namespace, expiration_age_days = DEFAULT_EXPIRATION_AGE_DAYS, dry_run = DEFAULT_DRY_RUN)
@namespace = namespace
@expiration_age_days = expiration_age_days
@dry_run = dry_run
# Here is potential to add more flexibility later.
@dtr_hostname = DEFAULT_DTR_HOSTNAME
@datetime_format = DEFAULT_TAG_DATETIME_FORMAT
@dtr_auth = nil
config_hash = JSON.parse(File.read("#{Dir.home}/.docker/config.json"))
if !config_hash["auths"]["https://#{@dtr_hostname}"].nil?
@dtr_auth = config_hash["auths"]["https://#{@dtr_hostname}"]["auth"]
elsif !config_hash["auths"][@dtr_hostname].nil?
@dtr_auth = config_hash["auths"][@dtr_hostname]["auth"]
end
raise "Cannot find credentials for #{@dtr_hostname} in #{Dir.home}/.docker/config.json" if @dtr_auth.nil?
end
def expired_tags_for_repo(repo)
timestamp_tags = get_timestamp_tags(repo)
if timestamp_tags.nil? || timestamp_tags.empty?
STDERR.puts "No images to be removed."
return []
end
return DockerImagePrune.determine_expired_tags(timestamp_tags, @expiration_age_days, @datetime_format)
end
# Delete the images from the given repo having the provided tags
def delete_tags_for_repo(repo, tags)
all_deleted = true
tags.each do | tag |
if dry_run
STDERR.puts "Image #{@namespace}/#{repo}:#{tag} would be removed."
else
response = RestClient::Request.execute(
method: :delete,
url: dtr_manifests_url(repo, tag),
headers: request_headers
)
if (response.code == 204)
STDERR.puts "Success. Removed expired tag: #{@namespace}/#{repo}:#{tag}"
else
STDERR.puts "Could not remove expired tag: #{@namespace}/#{repo}:#{tag}"
all_deleted = false
end
end
end
return all_deleted
end
# Query the repo and get all tags from it.
# Returns tags having format XXXX-YYYYY and assumes that such tags
# are timesamps tags.
def get_timestamp_tags(repo)
result_tags = []
response = RestClient::Request.execute(
method: :get,
url: dtr_tags_url(repo),
headers: request_headers
)
if (response.code == 200)
# puts response.body
j = JSON.parse(response.body)
j.each do | tag |
name = tag["name"]
datetimeString = name.split('-', 2)[1]
if datetimeString.nil? || datetimeString.empty?
# puts "Invalid datetime tag #{name}. Ignoring."
next
end
result_tags << name
end
end
return result_tags
end
# Input: a simple list of tags with nominal date format
# (e.g., XXXXX-YYYYY where YYYY is the datetime format)
# Ouput: the list of timestamp tags that are expired,
# taking account of the minimum 3 images we need to keep
# around.
#
# This is a class function so that it can more easily be called from a bash script,
# as in prune-local.sh.
#
def DockerImagePrune.determine_expired_tags(tags, expiration_age_days=DEFAULT_EXPIRATION_AGE_DAYS, datetime_format=DEFAULT_TAG_DATETIME_FORMAT)
target_tags = []
all_date_tags = []
tags.each do | tag |
begin
datetimeString = tag.split('-', 2)[1]
datetime_tag = Date.strptime(datetimeString, datetime_format)
all_date_tags << {tag: tag,
datetime: datetime_tag,
expired: (datetime_tag + expiration_age_days) < Date.today
}
rescue
STDERR.puts "Invalid datetime tag #{tag}. Ignoring."
next
end
end
# ensure tags are in timestamp order
all_date_tags.sort!{|x, y| x[:datetime] <=> y[:datetime]}
expired_tags = all_date_tags.select { |t| t[:expired] }
STDERR.puts "Total images with timestamp tags: #{all_date_tags.length}"
STDERR.puts "Total images to expire, nominally: #{expired_tags.length}"
if expired_tags.length == 0
# nothing to do
STDERR.puts "No images will be removed."
elsif all_date_tags.length >= expired_tags.length + DEFAULT_MINIMUM_IMAGES_TO_KEEP
# delete all the expired tags, because there are at least 3 other datetime tags not expired
STDERR.puts "All #{expired_tags.length} images with expired tags will be removed."
target_tags = expired_tags.map { | t | t[:tag] }
elsif all_date_tags.length <= DEFAULT_MINIMUM_IMAGES_TO_KEEP
# can't delete any of the expired tags
STDERR.puts "In order to keep a minimum of #{DEFAULT_MINIMUM_IMAGES_TO_KEEP} timestamped images, none will be removed."
target_tags = []
else
# delete only all_date_tags.length - 3 of the expired tags
keep = all_date_tags.length - DEFAULT_MINIMUM_IMAGES_TO_KEEP
STDERR.puts "Removing oldest #{keep} images in order to keep a minimum of #{DEFAULT_MINIMUM_IMAGES_TO_KEEP} timestamped images."
target_tags = expired_tags[0..(keep - 1)].map {|t| t[:tag]}
end
return target_tags
end
private
def request_headers
{"Authorization" => "Basic #{@dtr_auth}", "Content-Type" => "application/json"}
end
def dtr_tags_url(repo)
"https://#{@dtr_hostname}/api/v0/repositories/#{@namespace}/#{repo}/tags"
end
def dtr_repos_url
"https://#{@dtr_hostname}/api/v0/repositories/#{@namespace}"
end
def dtr_manifests_url(repo, tag)
"https://#{@dtr_hostname}/api/v0/repositories/#{@namespace}/#{repo}/tags/#{tag}"
end
end