gitlab-janitor 0.0.3 → 1.0.2.92939

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,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