gitlab-janitor 0.0.3 → 1.0.2.92939

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,120 @@
1
+ module GitlabJanitor
2
+ class ContainerCleaner < BaseCleaner
3
+
4
+ class Model < BaseCleaner::Model
5
+
6
+ def initialize(model)
7
+ super(model)
8
+
9
+ info['_Age'] = (Time.now - Time.at(created_at)).round(0)
10
+ end
11
+
12
+ def created_at
13
+ info['Created']
14
+ end
15
+
16
+ def name
17
+ @name ||= info['Names'].first.sub(%r{^/}, '')
18
+ end
19
+
20
+ def age
21
+ info['_Age']
22
+ end
23
+
24
+ def age_text
25
+ Fugit::Duration.parse(age).deflate.to_plain_s
26
+ end
27
+
28
+ end
29
+
30
+ attr_reader :excludes, :includes
31
+
32
+ def initialize(includes: [''], excludes: [''], **kwargs)
33
+ super(**kwargs)
34
+ @includes = includes
35
+ @excludes = excludes
36
+ @deadline = deadline
37
+ end
38
+
39
+ def do_clean(remove: false)
40
+ to_remove, keep = prepare(Docker::Container.all(all: true).map{|m| Model.new(m) })
41
+
42
+ return if to_remove.empty?
43
+
44
+ keep.each {|m| logger.debug(" KEEP #{m.name}") }
45
+
46
+ if remove
47
+ logger.info 'Removing containers...'
48
+ to_remove.each do |model|
49
+ return false if exiting?
50
+
51
+ logger.tagged(model.name) do
52
+ logger.debug ' Removing...'
53
+ log_exception('Stop') { model.stop }
54
+ log_exception('Wait') { model.wait(15) }
55
+ log_exception('Remove') { model.remove }
56
+ logger.debug ' Removing COMPLETED'
57
+ end
58
+ end
59
+ else
60
+ logger.info 'Skip removal due to dry run'
61
+ end
62
+ end
63
+
64
+ def prepare(containers)
65
+ @logger.debug("Selecting containers by includes #{@includes}...")
66
+ to_remove = select_by_name(containers)
67
+ if to_remove.empty?
68
+ @logger.info('Noting to remove.')
69
+ return [], containers
70
+ end
71
+ @logger.info("Selected containers: \n#{to_remove.map{|c| " + #{format_item(c)}" }.join("\n")}")
72
+
73
+ @logger.debug("Filtering containers by excludes #{@excludes}...")
74
+ to_remove = reject_by_name(to_remove)
75
+ if to_remove.empty?
76
+ @logger.info('Noting to remove.')
77
+ return [], containers
78
+ end
79
+ @logger.info("Filtered containers: \n#{to_remove.map{|c| " + #{format_item(c)}" }.join("\n")}")
80
+
81
+ @logger.info("Filtering containers by deadline: older than #{Fugit::Duration.parse(@deadline).deflate.to_plain_s}...")
82
+ to_remove = select_by_deadline(to_remove)
83
+ if to_remove.empty?
84
+ @logger.info('Noting to remove.')
85
+ return [], containers
86
+ end
87
+ @logger.info("Filtered containers: \n#{to_remove.map{|c| " + #{format_item(c)}" }.join("\n")}")
88
+
89
+ [to_remove, containers - to_remove]
90
+ end
91
+
92
+ def format_item(model)
93
+ "#{Time.at(model.created_at)} Age:#{model.age_text.ljust(10)} #{model.name.first(60).ljust(60)}"
94
+ end
95
+
96
+ def select_by_name(containers)
97
+ containers.select do |model|
98
+ @includes.any? do |pattern|
99
+ File.fnmatch(pattern, model.name)
100
+ end
101
+ end
102
+ end
103
+
104
+ def reject_by_name(containers)
105
+ containers.reject do |model|
106
+ @excludes.any? do |pattern|
107
+ File.fnmatch(pattern, model.name)
108
+ end
109
+ end
110
+ end
111
+
112
+ def select_by_deadline(containers)
113
+ containers.select do |model|
114
+ model.age > deadline
115
+ end
116
+ end
117
+
118
+ end
119
+ end
120
+
@@ -0,0 +1,66 @@
1
+ module GitlabJanitor
2
+ class ImageCleaner
3
+ class Store
4
+
5
+ attr_reader :logger, :filename, :images
6
+
7
+ def initialize(logger:, filename: './images.txt')
8
+ @filename = filename
9
+ @logger = logger
10
+ end
11
+
12
+ def parse_images
13
+ Docker::Image.all.map do |m|
14
+ tags = m.info.fetch('RepoTags', []) || []
15
+ tags = [m.info['id']] if tags.empty? || tags.first == '<none>:<none>'
16
+ m.info['RepoTags'] = tags
17
+
18
+ tags.map do |name|
19
+ Model.new(m, name, self)
20
+ end
21
+ end.flatten
22
+ end
23
+
24
+ def load
25
+ containers = Docker::Container.all(all: true).each_with_object({}) do |c, res|
26
+ res[c.info['ImageID']] = true
27
+ res[c.info['Image']] = true
28
+ end
29
+
30
+ File.open(@filename, 'a+') do |file|
31
+ i = 0
32
+ @images = file.readlines.select(&:present?).each_with_object({}) do |line, imgs|
33
+ i += 1
34
+ name, image_id, loaded_at = line.strip.split(' ')
35
+ loaded_at = Time.now.to_i if containers[name] || containers[image_id]
36
+
37
+ imgs[name] = { id: image_id, loaded_at: Time.at(loaded_at.to_i) }
38
+ rescue StandardError => e
39
+ logger.error "Unable to load from line #{i} '#{line}': #{e}"
40
+ end
41
+ end
42
+ @images
43
+ end
44
+
45
+ def image(img)
46
+ @images[img.name] ||= { id: img.id, loaded_at: Time.now }
47
+ end
48
+
49
+ def save(imgs = parse_images, skip_older: Time.at(0))
50
+ @images = @images.delete_if do |_k, img|
51
+ img[:loaded_at] < skip_older
52
+ end
53
+
54
+ imgs.each {|m| image(m) }
55
+
56
+ File.open(@filename, 'w+') do |file|
57
+ @images.each do |name, data|
58
+ file.puts("#{name} #{data[:id]} #{data[:loaded_at].to_i}")
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,140 @@
1
+ require 'open3'
2
+ require 'redis'
3
+
4
+ module GitlabJanitor
5
+ class ImageCleaner < BaseCleaner
6
+
7
+ class Model < BaseCleaner::Model
8
+
9
+ attr_reader :store
10
+
11
+ def initialize(model, name, store)
12
+ super(model)
13
+ @store = store
14
+ @name = name
15
+
16
+ info['_Age'] = (Time.now - Time.at(loaded_at)).round(0)
17
+ end
18
+
19
+ def loaded_at
20
+ store.image(self)[:loaded_at]
21
+ end
22
+
23
+ def name
24
+ @name || id
25
+ end
26
+
27
+ def age
28
+ info['_Age']
29
+ end
30
+
31
+ def age_text
32
+ Fugit::Duration.parse(age).deflate.to_plain_s
33
+ end
34
+
35
+ def id
36
+ info['id']
37
+ end
38
+
39
+ end
40
+
41
+ require_relative 'image_cleaner/store'
42
+
43
+ attr_reader :store
44
+
45
+ def initialize(image_store:, redis: nil, redis_list: 'gitlab-janitor:images_force_clean', **kwargs)
46
+ super(**kwargs)
47
+ @store = Store.new(filename: image_store, logger: logger)
48
+ @redis_url = redis
49
+ @redis_list = redis_list
50
+ end
51
+
52
+ def do_clean(remove: false)
53
+ store.load
54
+
55
+ force_clean(remove: remove)
56
+
57
+ to_remove, keep = prepare(store.parse_images)
58
+ store.save(skip_older: Time.now - @deadline)
59
+
60
+ return if to_remove.empty?
61
+
62
+ keep.each {|m| logger.debug(" KEEP #{m.name}") }
63
+
64
+ if remove
65
+ logger.info 'Removing images...'
66
+ to_remove.each do |model|
67
+ return false if exiting?
68
+
69
+ logger.tagged(model.name) do
70
+ logger.debug ' Removing...'
71
+ log_exception('Remove') { out, _status = Open3.capture2e("docker rmi #{model.name}"); logger.info(out) }
72
+ logger.debug ' Removing COMPLETED'
73
+ end
74
+ end
75
+ else
76
+ logger.info 'Skip removal due to dry run'
77
+ end
78
+ ensure
79
+ if remove
80
+ logger.info 'docker image prune -f'
81
+ out, _status = Open3.capture2e("docker image prune -f")
82
+ logger.info(out)
83
+ end
84
+ end
85
+
86
+ def prepare(images)
87
+ to_remove = images
88
+
89
+ @logger.info("Selected images: \n#{to_remove.map{|c| " + #{format_item(c)}" }.join("\n")}")
90
+
91
+ @logger.info("Filtering images by deadline: older than #{Fugit::Duration.parse(@deadline).deflate.to_plain_s}...")
92
+ to_remove = select_by_deadline(to_remove)
93
+ if to_remove.empty?
94
+ @logger.info('Noting to remove.')
95
+ return [], images
96
+ end
97
+ @logger.info("Filtered images: \n#{to_remove.map{|c| " !! #{format_item(c)}" }.join("\n")}")
98
+
99
+ [to_remove, (images - to_remove)]
100
+ end
101
+
102
+ def format_item(model)
103
+ "#{model.loaded_at} Age:#{model.age_text.ljust(13)} #{model.name.first(60).ljust(60)}"
104
+ end
105
+
106
+ def select_by_deadline(images)
107
+ images.select do |model|
108
+ model.age > deadline
109
+ end
110
+ end
111
+
112
+ def force_clean(remove: false)
113
+ return if @redis_url.nil?
114
+ logger.info("Force clean image from #{@redis_url}/#{@redis_url}...")
115
+
116
+ redis = Redis.new(url: @redis_url)
117
+ redis.ltrim(@redis_list, 0, 10)
118
+ now = Time.now
119
+ redis.lrange(@redis_list, 0, -1).each do |pair|
120
+ image, ts = pair.split('|')
121
+ if (now - Time.at(ts.to_i)) > 10.seconds
122
+ if remove
123
+ logger.info("Force clean #{image}")
124
+ log_exception('Remove') { out, _status = Open3.capture2e("docker rmi #{image}"); logger.info(out) }
125
+ else
126
+ logger.info("Skip Force clean #{image} due to dry run")
127
+ end
128
+ else
129
+ logger.info("Delay force clean #{image} by time")
130
+ end
131
+ rescue StandardError => e
132
+ logger.error("Error from force line: '#{pair}': #{e.inspect}")
133
+ end
134
+ rescue StandardError => e
135
+ logger.error("Unable to retrieve data from redis: #{e}")
136
+ end
137
+
138
+ end
139
+ end
140
+
@@ -1,11 +1,7 @@
1
- module GitlabJanitor
2
-
3
- class Fmt < ActiveSupport::Logger::SimpleFormatter
4
- def call(severity, timestamp, progname, msg)
5
- super
6
- end
7
- end
1
+ require 'logger'
2
+ require 'active_support/all'
8
3
 
4
+ module GitlabJanitor
9
5
  class Util
10
6
 
11
7
  TERM_SIGNALS = %w[INT TERM].freeze
@@ -21,27 +17,25 @@ module GitlabJanitor
21
17
  end
22
18
 
23
19
  def logger
24
- $logger
20
+ $logger ||= ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)).tap do |logger|
21
+ logger.level = ENV.fetch('LOG_LEVEL', Logger::INFO)
22
+ formatter = Logger::Formatter.new
23
+ formatter.extend ActiveSupport::TaggedLogging::Formatter
24
+ logger.formatter = formatter
25
+ end
25
26
  end
26
27
 
27
28
  def setup
28
29
  STDOUT.sync = true
29
30
  STDERR.sync = true
30
31
 
31
- $logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
32
- $logger.level = ENV.fetch('LOG_LEVEL', Logger::INFO)
33
- formatter = Logger::Formatter.new
34
- formatter.extend ActiveSupport::TaggedLogging::Formatter
35
- $logger.formatter = formatter
36
-
37
-
38
-
39
32
  initialize_signal_handlers
40
33
 
41
34
  String.class_eval do
42
35
  def to_bool
43
36
  return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
44
37
  return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
38
+
45
39
  raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
46
40
  end
47
41
  end
@@ -50,10 +44,10 @@ module GitlabJanitor
50
44
  def initialize_signal_handlers
51
45
  TERM_SIGNALS.each do |sig|
52
46
  trap(sig) do |*_args|
53
- TERM_SIGNALS.each do |sig|
54
- trap(sig) do |*_args|
55
- STDERR.puts 'Forcing exit!'
56
- Kernel::exit!(1)
47
+ TERM_SIGNALS.each do |s|
48
+ trap(s) do |*_args|
49
+ warn 'Forcing exit!'
50
+ Kernel.exit!(1)
57
51
  end
58
52
  end
59
53
 
@@ -66,4 +60,5 @@ module GitlabJanitor
66
60
  end
67
61
 
68
62
  end
69
- end
63
+ end
64
+
@@ -0,0 +1,6 @@
1
+ module GitlabJanitor
2
+
3
+ VERSION = '1.0.2'.freeze
4
+
5
+ end
6
+
@@ -0,0 +1,120 @@
1
+ module GitlabJanitor
2
+ class VolumeCleaner < BaseCleaner
3
+
4
+ class Model < BaseCleaner::Model
5
+
6
+ def initialize(model)
7
+ super(model)
8
+
9
+ info['_Age'] = (Time.now - Time.parse(created_at)).round(0)
10
+ end
11
+
12
+ def created_at
13
+ info['CreatedAt']
14
+ end
15
+
16
+ def name
17
+ info['Name']
18
+ end
19
+
20
+ def age
21
+ info['_Age']
22
+ end
23
+
24
+ def age_text
25
+ Fugit::Duration.parse(age).deflate.to_plain_s
26
+ end
27
+
28
+ def mountpoint
29
+ info['Mountpoint']
30
+ end
31
+
32
+ end
33
+
34
+ attr_reader :includes
35
+
36
+ def initialize(includes: [''], **kwargs)
37
+ super(**kwargs)
38
+ @includes = includes
39
+ end
40
+
41
+ def do_clean(remove: false)
42
+ to_remove, keep = prepare(Docker::Volume.all.map{|m| Model.new(m) })
43
+
44
+ return if to_remove.empty?
45
+
46
+ keep.each {|m| logger.debug(" KEEP #{m.name}") }
47
+
48
+ if remove
49
+ logger.info 'Removing volumes...'
50
+ to_remove.each do |model|
51
+ return false if exiting?
52
+
53
+ logger.tagged(model.name.first(10)) do
54
+ logger.debug ' Removing...'
55
+ log_exception('Remove') { model.remove }
56
+ logger.debug ' Removing COMPLETED'
57
+ end
58
+ end
59
+ else
60
+ logger.info 'Skip removal due to dry run'
61
+ end
62
+ end
63
+
64
+ def prepare(volumes)
65
+ @logger.debug('Selecting unnamed volumes...')
66
+ to_remove = select_unnamed(volumes)
67
+ if to_remove.empty?
68
+ @logger.info('Noting to remove.')
69
+ return [], volumes
70
+ end
71
+ @logger.info("Selected volumes: \n#{to_remove.map{|c| " + #{format_item(c)}" }.join("\n")}")
72
+
73
+ @logger.debug("Selecting volumes by includes #{@includes}...")
74
+ to_remove += select_by_name(volumes)
75
+ if to_remove.empty?
76
+ @logger.info('Noting to remove.')
77
+ return [], images
78
+ end
79
+ @logger.info("Selected volumes: \n#{to_remove.map{|c| " + #{format_item(c)}" }.join("\n")}")
80
+
81
+ @logger.info("Filtering volumes by deadline: older than #{Fugit::Duration.parse(@deadline).deflate.to_plain_s}...")
82
+ to_remove = select_by_deadline(to_remove)
83
+ if to_remove.empty?
84
+ @logger.info('Noting to remove.')
85
+ return [], volumes
86
+ end
87
+ @logger.info("Filtered volumes: \n#{to_remove.map{|c| " !! #{format_item(c)}" }.join("\n")}")
88
+
89
+ [to_remove, (volumes - to_remove)]
90
+ end
91
+
92
+ def format_item(model)
93
+ "#{Time.parse(model.created_at)} Age:#{model.age_text.ljust(13)} #{model.name.first(10).ljust(10)} #{model.mountpoint}"
94
+ end
95
+
96
+ def select_by_name(volumes)
97
+ volumes.select do |model|
98
+ @includes.any? do |pattern|
99
+ File.fnmatch(pattern, model.name)
100
+ end
101
+ end
102
+ end
103
+
104
+ SHA_RX = /^[a-zA-Z0-9]{64}$/.freeze
105
+
106
+ def select_unnamed(volumes)
107
+ volumes.select do |model|
108
+ SHA_RX.match(model.name)
109
+ end
110
+ end
111
+
112
+ def select_by_deadline(volumes)
113
+ volumes.select do |model|
114
+ model.age > deadline
115
+ end
116
+ end
117
+
118
+ end
119
+ end
120
+
@@ -0,0 +1,8 @@
1
+ require_relative 'gitlab_janitor/version'
2
+ require_relative 'gitlab_janitor/utils'
3
+ require_relative 'gitlab_janitor/base_cleaner'
4
+ require_relative 'gitlab_janitor/container_cleaner'
5
+ require_relative 'gitlab_janitor/volume_cleaner'
6
+ require_relative 'gitlab_janitor/image_cleaner'
7
+ require_relative 'gitlab_janitor/cache_cleaner'
8
+
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-janitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 1.0.2.92939
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samoilenko Yuri
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-01 00:00:00.000000000 Z
11
+ date: 2022-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: awesome_print
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -101,13 +115,27 @@ dependencies:
101
115
  - !ruby/object:Gem::Version
102
116
  version: '0'
103
117
  - !ruby/object:Gem::Dependency
104
- name: awesome_print
118
+ name: activesupport
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '6.0'
124
+ type: :runtime
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '6.0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: docker-api
105
133
  requirement: !ruby/object:Gem::Requirement
106
134
  requirements:
107
135
  - - ">="
108
136
  - !ruby/object:Gem::Version
109
137
  version: '0'
110
- type: :development
138
+ type: :runtime
111
139
  prerelease: false
112
140
  version_requirements: !ruby/object:Gem::Requirement
113
141
  requirements:
@@ -115,21 +143,21 @@ dependencies:
115
143
  - !ruby/object:Gem::Version
116
144
  version: '0'
117
145
  - !ruby/object:Gem::Dependency
118
- name: activesupport
146
+ name: fugit
119
147
  requirement: !ruby/object:Gem::Requirement
120
148
  requirements:
121
- - - "~>"
149
+ - - ">="
122
150
  - !ruby/object:Gem::Version
123
- version: '6.0'
151
+ version: '0'
124
152
  type: :runtime
125
153
  prerelease: false
126
154
  version_requirements: !ruby/object:Gem::Requirement
127
155
  requirements:
128
- - - "~>"
156
+ - - ">="
129
157
  - !ruby/object:Gem::Version
130
- version: '6.0'
158
+ version: '0'
131
159
  - !ruby/object:Gem::Dependency
132
- name: docker-api
160
+ name: redis
133
161
  requirement: !ruby/object:Gem::Requirement
134
162
  requirements:
135
163
  - - ">="
@@ -143,7 +171,7 @@ dependencies:
143
171
  - !ruby/object:Gem::Version
144
172
  version: '0'
145
173
  - !ruby/object:Gem::Dependency
146
- name: fugit
174
+ name: optparse
147
175
  requirement: !ruby/object:Gem::Requirement
148
176
  requirements:
149
177
  - - ">="
@@ -157,7 +185,7 @@ dependencies:
157
185
  - !ruby/object:Gem::Version
158
186
  version: '0'
159
187
  - !ruby/object:Gem::Dependency
160
- name: optparse
188
+ name: tzinfo-data
161
189
  requirement: !ruby/object:Gem::Requirement
162
190
  requirements:
163
191
  - - ">="
@@ -188,11 +216,15 @@ files:
188
216
  - docker-compose.yml
189
217
  - gitlab-janitor.gemspec
190
218
  - lib/gitlab-janitor.rb
191
- - lib/gitlab-janitor/base-cleaner.rb
192
- - lib/gitlab-janitor/container-cleaner.rb
193
- - lib/gitlab-janitor/utils.rb
194
- - lib/gitlab-janitor/version.rb
195
- - lib/gitlab-janitor/volume-cleaner.rb
219
+ - lib/gitlab_janitor.rb
220
+ - lib/gitlab_janitor/base_cleaner.rb
221
+ - lib/gitlab_janitor/cache_cleaner.rb
222
+ - lib/gitlab_janitor/container_cleaner.rb
223
+ - lib/gitlab_janitor/image_cleaner.rb
224
+ - lib/gitlab_janitor/image_cleaner/store.rb
225
+ - lib/gitlab_janitor/utils.rb
226
+ - lib/gitlab_janitor/version.rb
227
+ - lib/gitlab_janitor/volume_cleaner.rb
196
228
  homepage: https://github.com/RnD-Soft/gitlab-janitor
197
229
  licenses:
198
230
  - MIT
@@ -212,7 +244,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
212
244
  - !ruby/object:Gem::Version
213
245
  version: '0'
214
246
  requirements: []
215
- rubygems_version: 3.3.8
247
+ rubygems_version: 3.1.6
216
248
  signing_key:
217
249
  specification_version: 4
218
250
  summary: GitLab Janitor is a tool to automatically manage stalled containers when