ike-artifactory 0.0.1pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e54d885ec980d2c49686b6f3395adbf7ab854b433823bd0a4fdf903e9887db8b
4
+ data.tar.gz: e4af791e8b3751311af406bd43d29589de074b7bb2284f81195631782af8dd6e
5
+ SHA512:
6
+ metadata.gz: 4fdd3f97640d59e1bd33f1c66d0b16a6fdf94c7c3f76ff7c4b8ae0e8a7190fe7608498f5023f2e89220aa4a955653154e43219949e3cca14ba566ff456297a61
7
+ data.tar.gz: 383af6127fec80f4974c3ba60baafcabe6ceea00a2f514ef588325ff27f85e51b9341ef334b71d86b32d9f6dd0e8909f01ae06521058663a1bb82206a105ab72
data/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # IKE Artifactory
2
+
3
+ This gem provides an object-oriented interface to Artifactory API for managing objects in Artfactory,
4
+ particularly for cleaning up old Docker images.
5
+
6
+ ## Classes
7
+
8
+ This gem implements two classes:
9
+
10
+ * `IKE::Artifactory::Client`: Interfaces with the Artifactory API
11
+ * `IKE::Artifactory::DockerCleaner`: Uses `IKE::Artifactory::Client` to implement a single method called `cleanup!` that lets you specify a path in Artifactory that has Docker container images. The `cleanup!` method will delete all images except the following:
12
+ * a list of tags to be excluded (`tags_to_exclude`)
13
+ * any images less than a certain age (`days_old`)
14
+ * any the most recent N images, regardless of age (`most_recent_images`)
15
+
16
+ ## Utility scripts
17
+
18
+ Utility scripts that use these classes can be found in the `bin` directory:
19
+
20
+ * `cleaner.rb` - an interface to `IKE::Artifactory::DockerCleaner`; see [README.cleaner.md](README.cleaner.md)
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ ```ruby
27
+ gem 'ike-artifactory-ruby'
28
+ ```
29
+
30
+ And then execute:
31
+
32
+ $ bundle install
33
+
34
+ Or install it yourself as:
35
+
36
+ $ gem install ike-artifactory-ruby
37
+
38
+ ## Usage
39
+
40
+ ### IKE::Artifactory::Client
41
+
42
+ To create an instance of IKE::Artifactory::Client you will need to provide next parameters:
43
+ * **server**: Artifactory server URL.
44
+ * **repo_key**: Repository in Artifactory server.
45
+ * **user**: Username to be used to access repository.
46
+ * **password**: User's password.
47
+
48
+ #### Example
49
+ ```ruby
50
+ require 'ike-artifactory'
51
+
52
+ artifactory_client = IKE::Artifactory::Client.new(
53
+ :server => 'https://artifactory.mydomain.com',
54
+ :repo_key => 'repo-key-example',
55
+ :user => 'Ana',
56
+ :password => 'supersecret'
57
+ )
58
+
59
+ object_info = artifactory_client.get_object_info 'path/to/object'
60
+ ```
61
+
62
+ The output will be a hash with the proprieties of the queried object:
63
+ ```ruby
64
+ {"repo"=>"repo-key-example",
65
+ "path"=>"path/to/object",
66
+ "created"=>"2021-05-25T15:27:21.592-07:00",
67
+ "createdBy"=>"some-user",
68
+ "lastModified"=>"2021-05-25T15:27:21.592-07:00",
69
+ "modifiedBy"=>"other-userr",
70
+ "lastUpdated"=>"2021-05-25T15:27:21.592-07:00",
71
+ "children"=>
72
+ [{"uri"=>"/manifest.json", "folder"=>false},
73
+ {"uri"=>
74
+ "/sha256__4f07dd360c1b7e40c438e6437b2044bc31b4f6e5cf36b09a06b0c67e23dfc69d",
75
+ "folder"=>false},
76
+ {"uri"=>
77
+ "/sha256__70fb9965a23f2226fef622992fdf507b8333c61d68259766d4721cc4ba1e5dae",
78
+ "folder"=>false},
79
+ {"uri"=>
80
+ "/sha256__e0f9e11d6f9b3f5af2915fd4839ea0cd268ddccce28a788f54687b6a494770bb",
81
+ "folder"=>false}],
82
+ "uri"=>
83
+ "https://artifactory.mydomain.com:443/artifactory/api/storage/repo-key-example/path/to/object"}
84
+ ```
85
+
86
+ #### Methods
87
+
88
+ ##### `delete_object(path)`
89
+ Returns `true` if the object pointed by path was successfully deleted, otherwise `false`
90
+
91
+ ##### `get_subdirectories(path)`
92
+ Returns a list of subdirectories of the specified `path`.
93
+
94
+ ##### `get_object_age(path)`
95
+ Returns the age of the object specified by `path`, or -1 if the age of the object could not be determined (for example, if it does not exist).
96
+
97
+ ##### `get_object_info(path)`
98
+ Returns a hash with the proprieties of the queried object.
99
+
100
+ ##### `get_subdirectory_ages(path)`
101
+ Returns a hash whose keys are the names of the subdirectories of `path`, and whose values are the `lastModified` age in days of the directory in question.
102
+
103
+ ##### `get_images(path)`
104
+ Returns a hash whose keys are the names (tags) of the Docker images found in `path`, and whose values are the age of the Docker image in question. An entry in `path` is considered to be a Docker image if it contains the file identified by the `IKE::Artifactory::Client::IMAGE_MANIFEST` constant, which is `manifest.json`.
105
+
106
+ ### IKE::Artifactory::DockerCleaner
107
+
108
+ The constructor arguments of IKE::Artifactory::DockerCleaner are the following:
109
+ * `repo_host`: The URL of the Artifactory host, without the repo key included
110
+ * `repo_key` The repository to be cleaned
111
+ * `folder`: The repository path to be cleaned. `cleanup!` only cleans a single path (directory) and does not recurse
112
+ * `days_old`: The cutoff age for deletion of images. Any images less that `days_old` old will not be cleaned up.
113
+ * `most_recent_images`: The number of most recent container images to keep, regardless of age
114
+ * `tags_to_exclude`: List of Docker container tags to be excluded from deletion, regardless of age
115
+ * `user`: The username to be used to access repository
116
+ * `password`: User's password.
117
+ * `log_level` (optional): Logging verbosity, from the `Logger` Ruby core class. Defaults to ::Logger::INFO.
118
+ * `actually_delete` (optional): Whether to actually delete the images meeting the deletion criteria (truthy) or simply provide output about what would happen (falsy). Defaults to `false`.
119
+
120
+ Returns an array of image tags that would have been deleted (`actually_delete` = `false`) or were deleted (`actually_delete` = `true`):
121
+
122
+ ```ruby
123
+ ['tag-x', 'tag-y', 'tag-z']
124
+ ```
125
+
126
+ #### Example
127
+ ```ruby
128
+ require 'ike-artifactory'
129
+
130
+ images_to_delete = IKE::Artifactory::DockerCleaner.new(
131
+ :server => 'https://artifactory.mydomain.com',
132
+ :repo_key => 'repo-key-example',
133
+ :folder => 'path/to/folder',
134
+ :days_old => 30,
135
+ :most_recent_images => 5,
136
+ :tags_to_exclude => ['tag1', 'tag2', 'tag3'],
137
+ :user => 'Ana',
138
+ :password => 'supersecret'
139
+ ).cleanup!
140
+
141
+ puts "Not actually deleting images, but if I did I would have deleted these:"
142
+ images_to_delete.each do |i|
143
+ puts " #{i}"
144
+ end
145
+ ```
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'bundler/gem_tasks'
8
+ require 'rake/testtask'
9
+
10
+ Rake::TestTask.new(:test) do |t|
11
+ t.libs << 'lib'
12
+ t.libs << 'test'
13
+ t.pattern = 'test/**/*_test.rb'
14
+ t.verbose = false
15
+ end
16
+
17
+
18
+ task default: :test
data/bin/cleaner.rb ADDED
@@ -0,0 +1,65 @@
1
+ #!env ruby
2
+
3
+ $:.unshift(File.expand_path('../../lib', __FILE__))
4
+
5
+ require 'ike_artifactory'
6
+
7
+ actually_delete = false
8
+ if ARGV[0] == '--actually-delete'
9
+ ARGV.shift
10
+ actually_delete = true
11
+ end
12
+
13
+ unless [7,8].include?(ARGV.count)
14
+ STDERR.puts "Usage: $0 [--actually-delete] artifactory_url repo_key username password application_list image_exclude_list days_to_keep [most_recent_images_to_keep]"
15
+ exit 1
16
+ end
17
+
18
+ artifactory_url = ARGV.shift
19
+ repo_key = ARGV.shift
20
+ user = ARGV.shift
21
+ password = ARGV.shift
22
+ application_list = ARGV.shift
23
+ image_exclude_list = ARGV.shift
24
+ days_to_keep = ARGV.shift.to_i
25
+ most_recent_images_to_keep = ARGV.shift.to_i || 10
26
+
27
+ if days_to_keep <= 7
28
+ STDERR.puts "Invalid number of days_to_keep: #{days_to_keep}"
29
+ exit 2
30
+ end
31
+
32
+ apps = File.readlines(application_list).map { |line| line.chomp }
33
+
34
+ unless apps.count
35
+ STDERR.puts "No applications listed in #{application_list}, quitting"
36
+ exit 2
37
+ end
38
+
39
+ images_to_keep = File.readlines(image_exclude_list).each_with_object({}) do |line, keep|
40
+ parts = line.chomp.split(/:/)
41
+ if parts.length == 2
42
+ keep[parts[0]] ||= []
43
+ keep[parts[0]] << parts[1]
44
+ else
45
+ STDERR.puts "Can't parse #{line} as image to keep, aborting"
46
+ exit 3
47
+ end
48
+ keep
49
+ end
50
+
51
+ apps.each do |app|
52
+ puts "Cleaning #{app}"
53
+ cleaner = IKE::Artifactory::DockerCleaner.new(
54
+ actually_delete: actually_delete,
55
+ repo_host: artifactory_url,
56
+ repo_key: repo_key,
57
+ folder: app,
58
+ days_old: days_to_keep,
59
+ tags_to_exclude: images_to_keep[app],
60
+ user: user,
61
+ password: password,
62
+ most_recent_images: most_recent_images_to_keep
63
+ )
64
+ cleaner.cleanup!
65
+ end
@@ -0,0 +1,116 @@
1
+ require 'time'
2
+ require 'json'
3
+ require 'rest-client'
4
+ require 'uri'
5
+ require 'pry-byebug'
6
+
7
+ module IKE
8
+ module Artifactory
9
+ class Client
10
+
11
+ IMAGE_MANIFEST = 'manifest.json'
12
+
13
+ attr_accessor :server
14
+ attr_accessor :repo_key
15
+ attr_accessor :folder_path
16
+ attr_accessor :user
17
+ attr_accessor :password
18
+
19
+ def initialize(**args)
20
+ @server = args[:server]
21
+ @repo_key = args[:repo_key]
22
+ @user = args[:user]
23
+ @password = args[:password]
24
+
25
+ raise IKEArtifactoryClientNotReady.new(msg = 'Required attributes are missing. IKEArtifactoryGem not ready.') unless self.ready?
26
+ end
27
+
28
+ def delete_object(path)
29
+ fetch(path, method: :delete) do |response, request, result|
30
+ response.code == 204
31
+ end
32
+ end
33
+
34
+ def get_subdirectories(path)
35
+ get(path) do |response|
36
+ (response['children'] || []).select do |c|
37
+ c['folder']
38
+ end.map do |f|
39
+ f['uri'][1..]
40
+ end
41
+ end
42
+ end
43
+
44
+ def get_object_age(path)
45
+ get(path) do |response|
46
+ ( ( Time.now - Time.iso8601(response['lastModified']) ) / (24*60*60) ).to_int
47
+ end
48
+ end
49
+
50
+ def get_object_info(path)
51
+ get(path)
52
+ end
53
+
54
+ def get_subdirectory_ages(path)
55
+ get(path, prefix: "#{server}:443/ui/api/v1/ui/nativeBrowser/#{repo_key}") do |response|
56
+ (response['children'] || []).each_with_object({}) do |child, memo|
57
+ days_old = ( ( Time.now.to_i - (child['lastModified']/1000) ) / (24*60*60) ).to_int
58
+ memo[child['name']] = days_old
59
+ memo
60
+ end
61
+ end
62
+ end
63
+
64
+ def get_images(path)
65
+ get_subdirectory_ages(path).select do |(folder, _age)|
66
+ get_object_info([path, folder, IMAGE_MANIFEST].join('/'))
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def ready?
73
+ if ([server, repo_key].include? nil ) || ([user, password].include? nil )
74
+ return false
75
+ end
76
+ true
77
+ end
78
+
79
+ def fetch(path, method: :get, prefix: nil)
80
+ retval = nil # Work around Object#stub stomping on return values
81
+
82
+ prefix ||= "#{server}/artifactory/api/storage/#{repo_key}"
83
+
84
+ RestClient::Request.execute(
85
+ :method => method,
86
+ :url => "#{prefix}/#{path}",
87
+ :user => user,
88
+ :password => password
89
+ ) do |response, request, result|
90
+ retval =
91
+ if block_given?
92
+ yield response, request, result
93
+ else
94
+ [response, request, result]
95
+ end
96
+ end
97
+
98
+ retval
99
+ end
100
+
101
+ def get(path, prefix: nil)
102
+ fetch(path, prefix: prefix) do |response, request, result|
103
+ if response.code == 200
104
+ obj = JSON.parse(response.to_str)
105
+ if block_given?
106
+ yield obj
107
+ else
108
+ obj
109
+ end
110
+ end
111
+ end
112
+ end
113
+
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,82 @@
1
+ require 'uri'
2
+ require 'logger'
3
+
4
+ module IKE
5
+ module Artifactory
6
+
7
+ class DockerCleaner
8
+ attr_accessor :repo_host
9
+ attr_accessor :repo_key
10
+ attr_accessor :folder
11
+ attr_accessor :days_old
12
+ attr_accessor :tags_to_exclude
13
+
14
+ attr_reader :client # is not tested
15
+ attr_reader :most_recent_images # is not tested
16
+ attr_reader :logger # is not tested. Used for testing
17
+ attr_reader :actually_delete
18
+
19
+ def initialize(repo_host:, repo_key:, folder:, days_old:,
20
+ tags_to_exclude:, user:, password:, most_recent_images:,
21
+ log_level: ::Logger::INFO, actually_delete: false)
22
+
23
+ @repo_host = repo_host
24
+ @repo_key = repo_key
25
+ @folder = folder
26
+ @days_old = days_old
27
+ @tags_to_exclude = tags_to_exclude || []
28
+ @most_recent_images = most_recent_images
29
+
30
+ @actually_delete = actually_delete
31
+
32
+ @client = IKE::Artifactory::Client.new(
33
+ :server => repo_host,
34
+ :repo_key => repo_key,
35
+ :user => user,
36
+ :password => password
37
+ )
38
+
39
+ @logger = Logger.new(STDOUT)
40
+ logger.level = log_level
41
+ end
42
+
43
+ def cleanup!
44
+ deleted_images = []
45
+ tags = client.get_images(folder).sort_by { |_k,v| v }.to_h
46
+
47
+ too_new_to_delete = tags.keys[0...most_recent_images]
48
+
49
+ logger_prefix = "#{repo_host}/#{repo_key}"
50
+
51
+ tags.each do | tag, tag_days_old |
52
+
53
+ logger.debug "#{logger_prefix}: examining #{tag}"
54
+
55
+ if tags_to_exclude.include?(tag)
56
+ logger.info "#{logger_prefix}: tag #{tag} is explicitly excluded from cleanup"
57
+ next
58
+ end
59
+
60
+ if too_new_to_delete.include?(tag)
61
+ logger.info "#{logger_prefix}: tag #{tag} is one of the #{most_recent_images} most recent tags, preserving"
62
+ next
63
+ end
64
+
65
+ if tag_days_old < days_old
66
+ logger.info "#{logger_prefix}: tag #{tag} is less than #{days_old} days old, preserving"
67
+ next
68
+ end
69
+
70
+ logger.info "#{logger_prefix}: removing tag #{tag}"
71
+ if actually_delete
72
+ client.delete_object "#{folder}/#{tag}"
73
+ else
74
+ logger.info("#{logger_prefix}: Not actually deleting #{tag} because actually_delete is falsy")
75
+ end
76
+ deleted_images.append(tag)
77
+ end
78
+ deleted_images
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,9 @@
1
+ module IKE
2
+ module Artifactory
3
+ class IKEArtifactoryClientNotReady < StandardError
4
+ def initialize(msg="Unknown.")
5
+ super
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ module IKE
2
+ module Artifactory
3
+ VERSION = "0.0.1pre2"
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ require 'ike_artifactory/exceptions'
2
+ require 'ike_artifactory/version'
3
+ require 'ike_artifactory/client'
4
+ require 'ike_artifactory/docker_cleaner'
5
+
6
+
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ike-artifactory
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1pre2
5
+ platform: ruby
6
+ authors:
7
+ - Nick Marden
8
+ - Jack Newton
9
+ - Vicente Ramos Garcia
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2021-11-10 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rake
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rest-client
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: minitest
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: pry-byebug
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ description: Ruby gem for managing objects in Artfactory, particularly for cleaning
72
+ up old Docker images
73
+ email:
74
+ - nmarden@avvo.com
75
+ - jnewton@avvo.com
76
+ - vramosgarcia@avvo.com
77
+ executables: []
78
+ extensions: []
79
+ extra_rdoc_files: []
80
+ files:
81
+ - README.md
82
+ - Rakefile
83
+ - bin/cleaner.rb
84
+ - lib/ike_artifactory.rb
85
+ - lib/ike_artifactory/client.rb
86
+ - lib/ike_artifactory/docker_cleaner.rb
87
+ - lib/ike_artifactory/exceptions.rb
88
+ - lib/ike_artifactory/version.rb
89
+ homepage: https://github.com/internetbrands/ike-artifactory-ruby
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ allowed_push_host: https://rubygems.org
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">"
106
+ - !ruby/object:Gem::Version
107
+ version: 1.3.1
108
+ requirements: []
109
+ rubygems_version: 3.1.6
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Provides an object-oriented interface to Artifactory API.
113
+ test_files: []