docker_registry_cli 0.0.1
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 +7 -0
- data/auth/BasicAuthService.rb +26 -0
- data/auth/TokenAuthService.rb +41 -0
- data/bin/docker_registry.rb +140 -0
- data/commands/DockerRegistryCommand.rb +76 -0
- data/install.sh +17 -0
- data/requests/DockerRegistryRequest.rb +119 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 996d8a4754101bc04171017e1cd8173e49950333
|
4
|
+
data.tar.gz: a17f24f12d11e5a4249acae8d8a7f175a80cc803
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 72fde0e8eccd506b3da0f970f032f66376db75a68da743a3972cdd94e944ac0dae5a8e6d68b31a4fb4b8ff2478c952e85025a338ae075f72a38d7d590a09deea
|
7
|
+
data.tar.gz: 4c45321b3a9dc5a301ac13d1feb31bb2cb444069ff82c95493e3e6d90eb74ce471b9f1b4cf6193f50035078b30a6745648aca262f719defd3ebb90398f36faf9
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class BasicAuthService
|
2
|
+
|
3
|
+
@@requestToBeAuthed = nil
|
4
|
+
def initialize(requestToBeAuthed)
|
5
|
+
@@requestToBeAuthed = requestToBeAuthed
|
6
|
+
end
|
7
|
+
def byCredentials(user, pass)
|
8
|
+
@@requestToBeAuthed.class.basic_auth user, pass
|
9
|
+
end
|
10
|
+
|
11
|
+
def byToken(domain)
|
12
|
+
# load the docker config and see, if the domain is included there - reuse the auth token
|
13
|
+
config = JSON.parse(File.read(File.join(ENV['HOME'], '.docker/config.json')))
|
14
|
+
token = config['auths'][domain]['auth']
|
15
|
+
if(!token)
|
16
|
+
pp config if @@debug
|
17
|
+
throw('No auth token found for this domain')
|
18
|
+
end
|
19
|
+
|
20
|
+
pp "Using existing token from config.json "
|
21
|
+
|
22
|
+
# decorate our request object
|
23
|
+
# set the Authorization header directly, since it is already base64 encoded
|
24
|
+
@@requestToBeAuthed.class.headers['Authorization'] = "Basic #{token}"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class TokenAuthService
|
2
|
+
@@requestToBeAuthed = nil
|
3
|
+
def initialize(requestToBeAuthed)
|
4
|
+
@@requestToBeAuthed = requestToBeAuthed
|
5
|
+
end
|
6
|
+
|
7
|
+
def tokenAuth(responseToBeAuthed)
|
8
|
+
# retrieve how we will contact the token service
|
9
|
+
auth_description = responseToBeAuthed.headers()['www-authenticate']
|
10
|
+
options = {
|
11
|
+
:query => {
|
12
|
+
}
|
13
|
+
}
|
14
|
+
# get the url
|
15
|
+
m = /Bearer realm="(?<url>[^"]+)"/.match(auth_description)
|
16
|
+
url = m['url']
|
17
|
+
|
18
|
+
# get the service we will generate the tokens for
|
19
|
+
m = /service="(?<service>[^"]+)"/.match(auth_description)
|
20
|
+
if(m)
|
21
|
+
options[:query][:service] = m['service']
|
22
|
+
end
|
23
|
+
# the scope, importent if scopes are used in the token service
|
24
|
+
# this is not always set
|
25
|
+
m = /scope="(?<scope>[^"]+)"/.match(auth_description)
|
26
|
+
if(m)
|
27
|
+
options[:query][:scope] = m['scope']
|
28
|
+
end
|
29
|
+
|
30
|
+
unless(url)
|
31
|
+
puts "Could not extract auth information for Bearer token".colorize(:red) if @@debug
|
32
|
+
throw "Could not extract auth information for Bearer token"
|
33
|
+
end
|
34
|
+
# get the bearer token
|
35
|
+
response = HTTParty.get(url,options)
|
36
|
+
|
37
|
+
throw "Failed to authenticate, code #{response.code}" unless response.code == 200 || response.has_key?('token')
|
38
|
+
# decorate our request object
|
39
|
+
@@requestToBeAuthed.class.headers['Authorization'] = "Bearer #{response['token']}"
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'colorize'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
require_relative '../commands/DockerRegistryCommand'
|
8
|
+
|
9
|
+
# our defaults / defines
|
10
|
+
options = {:user => nil,:password => nil, :domain => nil, :debug => false}
|
11
|
+
ops = ['list','search', 'tags', 'delete_image', 'delete_tag']
|
12
|
+
|
13
|
+
# define our options and help
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: registry.rb [options]"
|
16
|
+
|
17
|
+
opts.on("-u", "--user USER", "optional, user to login") do |v|
|
18
|
+
options[:user] = v
|
19
|
+
end
|
20
|
+
opts.on("-p", "--password PASSWORD", "optional, password to login") do |v|
|
21
|
+
options[:password] = v
|
22
|
+
end
|
23
|
+
opts.on("--domain DOMAIN", "Set this to override the domain you defined in ~./docker_registry.yml") do |v|
|
24
|
+
options[:domain] = v
|
25
|
+
end
|
26
|
+
opts.on("-d", "--debug", "debug mode") do |v|
|
27
|
+
options[:debug] = v
|
28
|
+
end
|
29
|
+
opts.on("-h", "--help", "Prints this help") do
|
30
|
+
|
31
|
+
puts opts
|
32
|
+
puts "\nArguments:"
|
33
|
+
puts '<registry-domain> <operation> <value(optional)>'.colorize(:green)
|
34
|
+
puts "\nif you have set domain in ~/.docker_registry.yml you can ease it up like this:"
|
35
|
+
puts '<operation> <value(optional)>'.colorize(:green)
|
36
|
+
|
37
|
+
puts "\nOperations:"
|
38
|
+
puts 'list: list all available repositorys'
|
39
|
+
puts 'search <key>: search for a repository'
|
40
|
+
puts 'tags <repo-name>: list all tags of a repository'
|
41
|
+
puts 'delete_image <repo-name> <tag>: delete a image with all its tags'
|
42
|
+
puts 'delete_tag <repo-name> <tag>: delete a tag'
|
43
|
+
exit
|
44
|
+
end
|
45
|
+
end.parse!
|
46
|
+
|
47
|
+
# try to load values from tour configuration. Those get superseded by the arguments though
|
48
|
+
begin
|
49
|
+
config = YAML::load(File.read(File.join(ENV['HOME'], '.docker_registry.yml')))
|
50
|
+
if options[:debug]
|
51
|
+
puts 'Found config:'.colorize(:blue)
|
52
|
+
pp config
|
53
|
+
end
|
54
|
+
|
55
|
+
options[:domain] = config['domain'] if config['domain'] && !options[:domain]
|
56
|
+
options[:user] = config['user'] if config['user'] && !options[:user]
|
57
|
+
options[:password] = config['password'] if config['password'] && !options[:password]
|
58
|
+
rescue
|
59
|
+
# just no config, move on
|
60
|
+
puts 'No config found in ~/.docker_registry.yml . Create it and add domain:<> and optional user/password to avoid adding any arguments'.colorize(:light_white)
|
61
|
+
if !ARGV[0]
|
62
|
+
puts 'The first argument should be your registry domain without schema (HTTPS mandatory), optional with :port'.colorize(:red)
|
63
|
+
exit 1
|
64
|
+
else
|
65
|
+
options[:domain] = ARGV.shift
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# ensure a operation is set. Be aware, we used shift up there - so we always stick with 0
|
70
|
+
if !ARGV[0]
|
71
|
+
puts "Define the operation: #{ops.join(', ')}".colorize(:red)
|
72
|
+
exit 1
|
73
|
+
else
|
74
|
+
if !ops.include?(ARGV[0])
|
75
|
+
puts "This operation is yet not implemented. Select on of: #{ops.join(', ')}".colorize(:red)
|
76
|
+
exit 1
|
77
|
+
end
|
78
|
+
|
79
|
+
op = ARGV.shift
|
80
|
+
end
|
81
|
+
|
82
|
+
# print out some informations debug mode
|
83
|
+
if options[:debug]
|
84
|
+
pp options
|
85
|
+
puts "Operation: #{op}".colorize(:blue)
|
86
|
+
end
|
87
|
+
|
88
|
+
# configure our request handler
|
89
|
+
registry = DockerRegistryCommand.new(options[:domain], options[:user], options[:password], options[:debug])
|
90
|
+
|
91
|
+
# run the operations, which can actually have different amounts of mandatory arguments
|
92
|
+
case op
|
93
|
+
when 'delete_image'
|
94
|
+
if !ARGV[0]
|
95
|
+
puts 'Please define a image name'
|
96
|
+
exit 1
|
97
|
+
else
|
98
|
+
image_name = ARGV.shift
|
99
|
+
end
|
100
|
+
registry.delete_image(image_name)
|
101
|
+
when 'delete_tag'
|
102
|
+
if !ARGV[0]
|
103
|
+
puts 'Please define a image name'
|
104
|
+
exit 1
|
105
|
+
else
|
106
|
+
image_name = ARGV.shift
|
107
|
+
end
|
108
|
+
if !ARGV[0]
|
109
|
+
puts 'Please define a tag'
|
110
|
+
exit 1
|
111
|
+
else
|
112
|
+
tag = ARGV.shift
|
113
|
+
end
|
114
|
+
registry.delete_tag(image_name, tag)
|
115
|
+
when 'list'
|
116
|
+
registry.list
|
117
|
+
when 'search'
|
118
|
+
if !ARGV[0]
|
119
|
+
puts 'Please define a search key'
|
120
|
+
exit 1
|
121
|
+
else
|
122
|
+
key = ARGV.shift
|
123
|
+
registry.search(key)
|
124
|
+
end
|
125
|
+
when 'tags'
|
126
|
+
if !ARGV[0]
|
127
|
+
puts 'Please define a repo name'
|
128
|
+
exit 1
|
129
|
+
else
|
130
|
+
repo = ARGV.shift
|
131
|
+
tags = registry.tags(repo)
|
132
|
+
unless tags.nil?
|
133
|
+
tags.each { |tag|
|
134
|
+
puts tag
|
135
|
+
}
|
136
|
+
else
|
137
|
+
puts 'Not tags found'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require_relative '../requests/DockerRegistryRequest'
|
4
|
+
|
5
|
+
class DockerRegistryCommand < DockerRegistryRequest
|
6
|
+
### list all available repos
|
7
|
+
def list(search_key = nil)
|
8
|
+
response = send_get_request("/_catalog")
|
9
|
+
if response['repositories'].nil?
|
10
|
+
puts "no repositories found"
|
11
|
+
exit 1
|
12
|
+
else
|
13
|
+
response['repositories'].each { |repo|
|
14
|
+
puts repo if search_key.nil? || repo.include?(search_key)
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
### search for a specific repo using a key ( wildcard )
|
20
|
+
def search(search_key)
|
21
|
+
list(search_key)
|
22
|
+
end
|
23
|
+
|
24
|
+
### delete an image, so all its tags
|
25
|
+
### @see https://docs.docker.com/registry/spec/api/#deleting-an-image
|
26
|
+
def delete_image(image_name)
|
27
|
+
# be sure to enabel storage->delete->true in your registry, see https://github.com/docker/distribution/blob/master/docs/configuration.md
|
28
|
+
tags = tags(image_name)
|
29
|
+
if tags.nil?
|
30
|
+
puts "Image #{image_name} has no current tags, nothing to delete".red
|
31
|
+
exit
|
32
|
+
end
|
33
|
+
tags.each { |tag|
|
34
|
+
begin
|
35
|
+
puts "Deleting tag : #{tag}"
|
36
|
+
delete_tag(image_name, tag)
|
37
|
+
puts 'success'.green
|
38
|
+
rescue
|
39
|
+
puts 'failed'.red
|
40
|
+
end
|
41
|
+
}
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
### delete a tag, identified by image name and tag
|
47
|
+
### @see https://docs.docker.com/registry/spec/api/#deleting-an-image
|
48
|
+
def delete_tag(image_name, tag)
|
49
|
+
# be sure to enabel storage->delete->true in your registry, see https://github.com/docker/distribution/blob/master/docs/configuration.md
|
50
|
+
|
51
|
+
# fetch digest
|
52
|
+
digest = digest(image_name, tag)
|
53
|
+
unless digest
|
54
|
+
puts "Could not find digest from tag #{tag}".colorize(:red)
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
result = send_delete_request("/#{image_name}/manifests/#{digest}")
|
58
|
+
|
59
|
+
unless result.code == 202
|
60
|
+
puts "Could not delete tag #{tag}".colorize(:red)
|
61
|
+
exit 1
|
62
|
+
end
|
63
|
+
|
64
|
+
#result = send_delete_request("/#{image_name}/blobs/#{digest}")
|
65
|
+
#unless result.code == 202
|
66
|
+
# puts "Could not delete blob from digest #{digest}".colorize(:red)
|
67
|
+
# exit 1
|
68
|
+
#end
|
69
|
+
end
|
70
|
+
|
71
|
+
### list all tags of a repo
|
72
|
+
def tags(repo)
|
73
|
+
result = send_get_request("/#{repo}/tags/list")
|
74
|
+
return result['tags']
|
75
|
+
end
|
76
|
+
end
|
data/install.sh
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
echo "Please enter your registry domain like docker.mycompany.com - do not add https://, you can add a port"
|
4
|
+
read domain
|
5
|
+
echo "domain: $domain" > ~/.docker_registry.yml
|
6
|
+
echo "created configuration"
|
7
|
+
echo "installing gems"
|
8
|
+
bundle install
|
9
|
+
echo "Should i create a symlink into /usr/local/bin (needs sudo permissions)"
|
10
|
+
select yn in "Yes" "No"; do
|
11
|
+
case $yn in
|
12
|
+
Yes ) sudo ln -fs `pwd`/bin/docker_registry.rb /usr/local/bin/docker_registry; break;;
|
13
|
+
No ) echo "skipped";;
|
14
|
+
esac
|
15
|
+
done
|
16
|
+
|
17
|
+
echo "HINT: Ensure you run docker login $domain to save your credentials encrypted"
|
@@ -0,0 +1,119 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'json'
|
5
|
+
require 'httparty'
|
6
|
+
require 'yaml'
|
7
|
+
require 'pp'
|
8
|
+
|
9
|
+
require_relative '../auth/TokenAuthService'
|
10
|
+
require_relative '../auth/BasicAuthService'
|
11
|
+
|
12
|
+
class DockerRegistryRequest
|
13
|
+
include HTTParty
|
14
|
+
format :json
|
15
|
+
headers 'Content-Type' => 'application/json'
|
16
|
+
headers 'Accept' => 'application/json'
|
17
|
+
@@debug = false
|
18
|
+
|
19
|
+
def initialize(domain, user = nil, pass = nil, debug = false)
|
20
|
+
@@debug = debug
|
21
|
+
self.class.base_uri "https://#{domain}/v2"
|
22
|
+
handle_preauth(domain, user, pass)
|
23
|
+
end
|
24
|
+
|
25
|
+
def handle_preauth(domain, user = nil, pass = nil)
|
26
|
+
# Only BasicAuth is supported
|
27
|
+
authService = BasicAuthService.new(self)
|
28
|
+
begin
|
29
|
+
if user && pass
|
30
|
+
# this will base64 encode automatically
|
31
|
+
authService.byCredentials(user, pass)
|
32
|
+
else
|
33
|
+
authService.byToken(domain)
|
34
|
+
end
|
35
|
+
rescue Exception
|
36
|
+
puts "No BasicAuth pre-auth available, will try to use different auth-services later".green
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
### check if the login actually will succeed
|
42
|
+
def authenticate(response)
|
43
|
+
headers = response.headers()
|
44
|
+
begin
|
45
|
+
if headers.has_key?('www-authenticate')
|
46
|
+
auth_description = headers['www-authenticate']
|
47
|
+
if auth_description.match('Bearer realm=')
|
48
|
+
authService = TokenAuthService.new(self)
|
49
|
+
authService.tokenAuth(response)
|
50
|
+
else
|
51
|
+
throw "Auth method not supported #{auth_description}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue Exception
|
55
|
+
puts "Authentication failed".colorize(:red)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
## sends a get request, authenticates if needed
|
60
|
+
def send_get_request(path, options = {})
|
61
|
+
# we try to send the request. if it fails due to auth, we need the returned scope
|
62
|
+
# thats why we first try to do it without auth, then reusing the scope from the response
|
63
|
+
response = self.class.get(path, options)
|
64
|
+
# need auth
|
65
|
+
case (response.code)
|
66
|
+
when 200
|
67
|
+
# just continue
|
68
|
+
when 401
|
69
|
+
authenticate(response)
|
70
|
+
response = self.class.get(path, options)
|
71
|
+
else
|
72
|
+
end
|
73
|
+
unless response.code == 200
|
74
|
+
throw "Could not finish request, status #{response.code}"
|
75
|
+
end
|
76
|
+
return response
|
77
|
+
end
|
78
|
+
|
79
|
+
## sends a delete request, authenticates if needed
|
80
|
+
def send_delete_request(path)
|
81
|
+
response = self.class.delete(path)
|
82
|
+
# need auth
|
83
|
+
case (response.code)
|
84
|
+
when 200
|
85
|
+
# just continue
|
86
|
+
when 401
|
87
|
+
authenticate(response)
|
88
|
+
response = self.class.delete(path)
|
89
|
+
else
|
90
|
+
end
|
91
|
+
if response.code != 200 && response.code != 202
|
92
|
+
throw "Could not finish request, status #{response.code}"
|
93
|
+
end
|
94
|
+
return response
|
95
|
+
end
|
96
|
+
|
97
|
+
### returns the digest for a tag
|
98
|
+
### @see https://docs.docker.com/registry/spec/api/#pulling-an-image
|
99
|
+
def digest(image_name, tag)
|
100
|
+
options = {
|
101
|
+
:headers => {
|
102
|
+
'Accept' => 'application/vnd.docker.distribution.manifest.v2+json'
|
103
|
+
}
|
104
|
+
}
|
105
|
+
begin
|
106
|
+
response = send_get_request("/#{image_name}/manifests/#{tag}", options)
|
107
|
+
rescue
|
108
|
+
puts "Could not find digest for image #{image_name} with tag #{tag}".colorize(:red)
|
109
|
+
exit 1
|
110
|
+
end
|
111
|
+
|
112
|
+
unless response.code == 200
|
113
|
+
puts "Could not find digest for image #{image_name} with tag #{tag}".colorize(:red)
|
114
|
+
exit 1
|
115
|
+
end
|
116
|
+
|
117
|
+
return response.headers['docker-content-digest']
|
118
|
+
end
|
119
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: docker_registry_cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Eugen Mayer
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-06-20 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: This cli-tool lets you query your private docker registry for different
|
14
|
+
things. For now, there is no tool provided by docker to do so
|
15
|
+
email: eugen.mayer@kontextwork.de
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- auth/BasicAuthService.rb
|
21
|
+
- auth/TokenAuthService.rb
|
22
|
+
- bin/docker_registry.rb
|
23
|
+
- commands/DockerRegistryCommand.rb
|
24
|
+
- install.sh
|
25
|
+
- requests/DockerRegistryRequest.rb
|
26
|
+
homepage:
|
27
|
+
licenses:
|
28
|
+
- GPL
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 2.4.5.1
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: Docker Registry Cli - Search your docker registry from the cli
|
50
|
+
test_files: []
|