docker-cleaner 0.3.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 +7 -0
- data/.gitignore +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +24 -0
- data/bin/docker-cleaner +87 -0
- data/docker-cleaner.gemspec +21 -0
- data/lib/docker_cleaner.rb +9 -0
- data/lib/docker_cleaner/containers.rb +46 -0
- data/lib/docker_cleaner/images.rb +117 -0
- data/lib/docker_cleaner/version.rb +3 -0
- metadata +69 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 03ac980101dedfb38b309cc3464a839141a54361
|
4
|
+
data.tar.gz: 8accceff7c76e77e357d1cfd81bd0e6ded224789
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 15d8c9d9469c5a62ad7931f1a9665270ac607d50b1223c33da84954f0e1674842391010b5b8442dd3afab0e3a5ef1288f0676a85b6ba9481f86af69668fabdd1
|
7
|
+
data.tar.gz: ecaa894f663a25bee97164aaff6b1f6b49edfcc3c372b7cc37146362b6ef44cb455a5bce50de280bcff0c1a99abc1813bd7675310e69d49f8e0d0eb2ae373f2e
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
docker-cleaner (0.1)
|
5
|
+
docker-api (~> 1.17)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
docker-api (1.21.4)
|
11
|
+
excon (>= 0.38.0)
|
12
|
+
json
|
13
|
+
excon (0.45.3)
|
14
|
+
json (1.8.2)
|
15
|
+
|
16
|
+
PLATFORMS
|
17
|
+
ruby
|
18
|
+
|
19
|
+
DEPENDENCIES
|
20
|
+
docker-api (~> 1.18)
|
21
|
+
docker-cleaner!
|
22
|
+
|
23
|
+
BUNDLED WITH
|
24
|
+
1.12.5
|
data/bin/docker-cleaner
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
path = File.expand_path(File.join(File.dirname(__FILE__), 'lib'))
|
4
|
+
$LOAD_PATH << path
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'bundler/setup'
|
8
|
+
Bundler.require :default
|
9
|
+
require 'docker'
|
10
|
+
|
11
|
+
require 'optparse'
|
12
|
+
require 'logger'
|
13
|
+
|
14
|
+
STOP_DOCKER_CLEANER_FILE = '/tmp/stop-docker-cleaner'.freeze
|
15
|
+
|
16
|
+
options = {}
|
17
|
+
options[:registry] = ENV["REGISTRY"]
|
18
|
+
options[:prefix] = ENV["PREFIX"]
|
19
|
+
options[:log] = ENV["LOG_FILE"]
|
20
|
+
options[:docker] = ENV["DOCKER_HOST"]
|
21
|
+
options[:delete_delay] = ENV["DELETE_DELAY"]
|
22
|
+
options[:registries] = ENV["REGISTRIES"] || options[:registry]
|
23
|
+
options[:retention] = ENV["RETENTION"]
|
24
|
+
|
25
|
+
OptionParser.new do |opts|
|
26
|
+
opts.banner = "Usage: docker_clean [options]"
|
27
|
+
|
28
|
+
opts.on("--delete-delay=DELAY", "Delay in seconds between container/image deletion") do |r|
|
29
|
+
options[:delete_delay] = r
|
30
|
+
end
|
31
|
+
opts.on("-pPREFIX", "--prefix=PREFIX", "Prefix of images your want to clean") do |r|
|
32
|
+
options[:prefix] = r
|
33
|
+
end
|
34
|
+
opts.on("-rREGISTRY", "--registry=REGISTRY", "Registry") do |r|
|
35
|
+
options[:registries] = r
|
36
|
+
end
|
37
|
+
opts.on("--registries=REGISTRIES", "Registries") do |r|
|
38
|
+
options[:registries] = r
|
39
|
+
end
|
40
|
+
opts.on("-lLOG", "--log=LOG", "Log file") do |r|
|
41
|
+
options[:log] = r
|
42
|
+
end
|
43
|
+
opts.on("-dDOCKER", "--docker=DOCKER", "Docker endpoint") do |r|
|
44
|
+
options[:docker] = r
|
45
|
+
end
|
46
|
+
opts.on("--retention=RETENTION", "How long images should be kept before deletion (in hours)") do |r|
|
47
|
+
options[:retention] = r
|
48
|
+
end
|
49
|
+
|
50
|
+
opts.on("--force", "Force the clean even if the #{STOP_DOCKER_CLEANER_FILE} file is present") do |r|
|
51
|
+
options[:force] = true
|
52
|
+
end
|
53
|
+
end.parse!
|
54
|
+
|
55
|
+
logger = Logger.new options[:log] || $stdout
|
56
|
+
|
57
|
+
if options[:registries].nil?
|
58
|
+
$stderr.puts "--registries option should be filled"
|
59
|
+
exit -1
|
60
|
+
end
|
61
|
+
|
62
|
+
options[:registries] = options[:registries].split(",").map(&:chomp)
|
63
|
+
|
64
|
+
options[:retention] = options[:retention].to_i
|
65
|
+
options[:retention] = 6 if options[:retention] == 0
|
66
|
+
|
67
|
+
Docker.url = options[:docker] || "http://localhost:4243"
|
68
|
+
Docker.options = { read_timeout: 300, write_timeout: 300 }
|
69
|
+
|
70
|
+
if File.file? STOP_DOCKER_CLEANER_FILE
|
71
|
+
logger.info 'Stop docker cleaner file is present'
|
72
|
+
|
73
|
+
if !options[:force]
|
74
|
+
logger.info 'Aborting'
|
75
|
+
exit 0
|
76
|
+
else
|
77
|
+
logger.info 'Force flag is present, continuing...'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
require 'docker_cleaner'
|
82
|
+
|
83
|
+
DockerCleaner.run(
|
84
|
+
options[:registries], options[:prefix], logger,
|
85
|
+
delay: options[:delete_delay].to_i,
|
86
|
+
retention: options[:retention],
|
87
|
+
)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/docker_cleaner/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "docker-cleaner"
|
6
|
+
s.version = DockerCleaner::VERSION
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Leo Unbekandt"]
|
9
|
+
s.email = ["leo@scalingo.com"]
|
10
|
+
s.homepage = "https://github.com/Scalingo/docker-cleaner"
|
11
|
+
s.summary = "Small utility to clean old containers and images"
|
12
|
+
s.description = "Small utility to clean old docker data, containers according to some settings"
|
13
|
+
s.license = "MIT"
|
14
|
+
|
15
|
+
s.add_dependency "docker-api", "~> 1.0"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
19
|
+
s.require_path = 'lib'
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'docker_cleaner/containers'
|
2
|
+
require 'docker_cleaner/images'
|
3
|
+
|
4
|
+
module DockerCleaner
|
5
|
+
def self.run(registries, prefix, logger, opts)
|
6
|
+
DockerCleaner::Containers.new(logger, opts).run
|
7
|
+
DockerCleaner::Images.new(registries, prefix, logger, opts).run
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module DockerCleaner
|
2
|
+
class Containers
|
3
|
+
def initialize(logger, opts = {})
|
4
|
+
@logger = logger
|
5
|
+
@delay = opts.fetch(:delay, 0)
|
6
|
+
end
|
7
|
+
|
8
|
+
def remove(container)
|
9
|
+
@logger.info "Remove #{container.id[0...10]} - #{container.info["Image"]} - #{container.info["Names"][0]}"
|
10
|
+
container.remove v: true
|
11
|
+
@logger.info "Remove #{container.id[0...10]} - #{container.info["Image"]} - #{container.info["Names"][0]}... OK"
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
# Remove stopped container which stopped with code '0'
|
16
|
+
two_hours_ago = Time.now.to_i - 2 * 3600
|
17
|
+
Docker::Container.all(all: true).select{ |container|
|
18
|
+
status = container.info["Status"]
|
19
|
+
(status == "Created" && container.info["Created"].to_i < two_hours_ago) ||
|
20
|
+
(status.include?("Exited (") && container.info["Created"].to_i < two_hours_ago)
|
21
|
+
}.each do |container|
|
22
|
+
remove(container)
|
23
|
+
sleep(@delay)
|
24
|
+
end
|
25
|
+
|
26
|
+
containers_per_app = {}
|
27
|
+
Docker::Container.all(all: true).select{ |container|
|
28
|
+
container.info["Status"].include?("Exited")
|
29
|
+
}.each{ |container|
|
30
|
+
app = container.info["Image"].split(":", 2)[0]
|
31
|
+
if containers_per_app[app].nil?
|
32
|
+
containers_per_app[app] = [container]
|
33
|
+
else
|
34
|
+
containers_per_app[app] << container
|
35
|
+
end
|
36
|
+
}
|
37
|
+
containers_per_app.each do |app, containers|
|
38
|
+
containers.shift
|
39
|
+
containers.each do |container|
|
40
|
+
remove(container)
|
41
|
+
sleep(@delay)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module DockerCleaner
|
2
|
+
class Images
|
3
|
+
def initialize registries, prefix, logger, opts = {}
|
4
|
+
@prefix = prefix || ""
|
5
|
+
@registries = registries
|
6
|
+
@logger = logger
|
7
|
+
@delay = opts.fetch(:delay, 0)
|
8
|
+
@retention = Time.now.to_i - opts.fetch(:retention, 6) * 3600
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
clean_old_images
|
13
|
+
clean_unnamed_images
|
14
|
+
clean_unused_images
|
15
|
+
end
|
16
|
+
|
17
|
+
def clean_unnamed_images
|
18
|
+
Docker::Image.all.select do |image|
|
19
|
+
image.info["RepoTags"].nil? || image.info["RepoTags"][0] == "<none>:<none>"
|
20
|
+
end.each do |image|
|
21
|
+
@logger.info "Remove unnamed image #{image.id[0...10]}"
|
22
|
+
begin
|
23
|
+
image.remove
|
24
|
+
sleep(@delay)
|
25
|
+
rescue Docker::Error::NotFoundError
|
26
|
+
rescue Docker::Error::ConflictError => e
|
27
|
+
@logger.warn "Conflict when removing #{image.id[0...10]}"
|
28
|
+
@logger.warn " ! #{e.message}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def clean_old_images
|
34
|
+
apps = images_with_latest
|
35
|
+
apps.each do |app, images|
|
36
|
+
if app =~ /.*-tmax$/
|
37
|
+
next
|
38
|
+
end
|
39
|
+
images.each do |i|
|
40
|
+
unless i.info["Created"] == apps["#{app}-tmax"]
|
41
|
+
@logger.info "Remove #{i.info['RepoTags'][0]} => #{i.id[0...10]}"
|
42
|
+
begin
|
43
|
+
i.remove
|
44
|
+
sleep(@delay)
|
45
|
+
rescue Docker::Error::NotFoundError
|
46
|
+
rescue Docker::Error::ConflictError => e
|
47
|
+
@logger.warn "Conflict when removing #{i.info['RepoTags'][0]} - ID: #{i.id[0...10]}"
|
48
|
+
@logger.warn " ! #{e.message}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def images_with_latest
|
56
|
+
images ||= Docker::Image.all
|
57
|
+
apps = {}
|
58
|
+
|
59
|
+
images.each do |i|
|
60
|
+
# RepoTags can be nil sometimes, in this case we ignore the image
|
61
|
+
next if i.info["RepoTags"].nil?
|
62
|
+
if registries_include?(i.info["RepoTags"][0])
|
63
|
+
name = i.info["RepoTags"][0].split(":")[0]
|
64
|
+
tmax = "#{name}-tmax"
|
65
|
+
|
66
|
+
if apps[name].nil?
|
67
|
+
apps[name] = [i]
|
68
|
+
else
|
69
|
+
apps[name] << i
|
70
|
+
end
|
71
|
+
|
72
|
+
if apps[tmax].nil?
|
73
|
+
apps[tmax] = i.info["Created"]
|
74
|
+
elsif apps[tmax] < i.info["Created"]
|
75
|
+
apps[tmax] = i.info["Created"]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
apps
|
80
|
+
end
|
81
|
+
|
82
|
+
def clean_unused_images
|
83
|
+
used_images = Docker::Container.all.map{|c| c.info["Image"]}.select{|i| registries_include?(i) }.uniq
|
84
|
+
# Images older than 2 months
|
85
|
+
images = Docker::Image.all.select{|i| i.info["RepoTags"] && registries_include?(i.info["RepoTags"][0]) && i.info["Created"] < @retention }
|
86
|
+
image_repos = images.map{|i| i.info["RepoTags"][0]}
|
87
|
+
unused_images = image_repos - used_images
|
88
|
+
|
89
|
+
unused_images.each do |i|
|
90
|
+
image = images.select{|docker_image| docker_image.info["RepoTags"][0] == i}[0]
|
91
|
+
@logger.info "Remove unused image #{image.info['RepoTags'][0]} => #{image.id[0...10]}"
|
92
|
+
begin
|
93
|
+
image.remove
|
94
|
+
sleep(@delay)
|
95
|
+
rescue Docker::Error::NotFoundError
|
96
|
+
rescue Docker::Error::ConflictError => e
|
97
|
+
@logger.warn "Conflict when removing #{image.info['RepoTags'][0]} - ID: #{image.id[0...10]}"
|
98
|
+
@logger.warn " ! #{e.message}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
protected
|
104
|
+
|
105
|
+
def registries_include?(image)
|
106
|
+
if image.nil? || image == ''
|
107
|
+
return false
|
108
|
+
end
|
109
|
+
@registries.each do |registry|
|
110
|
+
if image =~ /^#{registry}\/#{@prefix}/
|
111
|
+
return true
|
112
|
+
end
|
113
|
+
end
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
metadata
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docker-cleaner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Leo Unbekandt
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-12-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: docker-api
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
description: Small utility to clean old docker data, containers according to some
|
28
|
+
settings
|
29
|
+
email:
|
30
|
+
- leo@scalingo.com
|
31
|
+
executables:
|
32
|
+
- docker-cleaner
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".gitignore"
|
37
|
+
- Gemfile
|
38
|
+
- Gemfile.lock
|
39
|
+
- bin/docker-cleaner
|
40
|
+
- docker-cleaner.gemspec
|
41
|
+
- lib/docker_cleaner.rb
|
42
|
+
- lib/docker_cleaner/containers.rb
|
43
|
+
- lib/docker_cleaner/images.rb
|
44
|
+
- lib/docker_cleaner/version.rb
|
45
|
+
homepage: https://github.com/Scalingo/docker-cleaner
|
46
|
+
licenses:
|
47
|
+
- MIT
|
48
|
+
metadata: {}
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 2.6.11
|
66
|
+
signing_key:
|
67
|
+
specification_version: 4
|
68
|
+
summary: Small utility to clean old containers and images
|
69
|
+
test_files: []
|