ike-artifactory 0.0.1pre2

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 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: []