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.
@@ -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
@@ -0,0 +1,2 @@
1
+ /.bundle
2
+ /binstubs
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'docker-api', '~> 1.18'
4
+ gem 'docker-cleaner', path: "."
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module DockerCleaner
2
+ VERSION = "0.3.0"
3
+ 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: []