tagfish 1.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/.gitignore +14 -0
- data/Dockerfile +23 -0
- data/Gemfile +4 -0
- data/README.md +178 -0
- data/Rakefile +39 -0
- data/bin/tagfish +32 -0
- data/lib/tagfish.rb +5 -0
- data/lib/tagfish/api_call.rb +41 -0
- data/lib/tagfish/docker_api.rb +113 -0
- data/lib/tagfish/docker_http_auth.rb +31 -0
- data/lib/tagfish/docker_uri.rb +64 -0
- data/lib/tagfish/search_command.rb +32 -0
- data/lib/tagfish/tags.rb +26 -0
- data/lib/tagfish/tags_command.rb +37 -0
- data/lib/tagfish/tags_logic.rb +52 -0
- data/lib/tagfish/tokeniser.rb +37 -0
- data/lib/tagfish/update/differ.rb +13 -0
- data/lib/tagfish/update/update_command.rb +31 -0
- data/lib/tagfish/update/updater.rb +53 -0
- data/lib/tagfish/update/uri_filters.rb +20 -0
- data/lib/tagfish/version.rb +3 -0
- data/logo.jpg +0 -0
- data/tagfish.gemspec +27 -0
- metadata +152 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 801be00ea36e0cc2c98c535546c45f1025768ca0
|
4
|
+
data.tar.gz: 9ce46a51783f66f42f9da2f4d5a5cebc6e459fd4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ad70b423105855d754bed1ad62d9291dbc18ac4ce113e38e3825aa72765d792572767a6f3df90f2c20c0af5677358cc9281f6499dbb49fb303e5ca312740c182
|
7
|
+
data.tar.gz: 33dbd5a8faf479d8a10364b7980731bb9c1823915d4aab0d08a4ac6d3a018fde857b22c22c950359bce3c2086fb9fd58eb92dafc358bb3580a753b5bc2ce6efe
|
data/.gitignore
ADDED
data/Dockerfile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
FROM alpine:edge
|
2
|
+
MAINTAINER Clement Labbe <clement.labbe@rea-group.com>
|
3
|
+
|
4
|
+
RUN apk add --update ruby=2.2.3-r1 \
|
5
|
+
ruby-dev=2.2.3-r1 \
|
6
|
+
ruby-io-console=2.2.3-r1 \
|
7
|
+
diffutils \
|
8
|
+
linux-headers \
|
9
|
+
build-base \
|
10
|
+
ca-certificates=20150426-r3 && \
|
11
|
+
rm /var/cache/apk/* && \
|
12
|
+
rm -rf /usr/share/ri
|
13
|
+
|
14
|
+
RUN echo -e 'gem: --no-rdoc --no-ri' > /etc/gemrc \
|
15
|
+
gem update --system 2.4.8 && \
|
16
|
+
gem install bundler -v 1.10.6 && \
|
17
|
+
rm -rf /usr/share/ri
|
18
|
+
|
19
|
+
COPY pkg/tagfish-latest.gem /cwd/
|
20
|
+
WORKDIR /cwd
|
21
|
+
RUN gem install tagfish-latest.gem
|
22
|
+
|
23
|
+
ENTRYPOINT ["tagfish"]
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
# Tagfish
|
2
|
+
|
3
|
+

|
4
|
+
|
5
|
+
Tagfish is a CLI tool to interact with Docker registries.
|
6
|
+
|
7
|
+
Features include:
|
8
|
+
|
9
|
+
- List all the tags of a given Docker repository
|
10
|
+
- Return the most recent explicit tag of a repository
|
11
|
+
- Update a file with the newest tags
|
12
|
+
- Search for a repository
|
13
|
+
- Authenticate by reading native Docker config file
|
14
|
+
- Works against hub.docker.com and private registries
|
15
|
+
- Supports Docker Registry/Distribution API v1 and v2
|
16
|
+
|
17
|
+
To use Tagfish against a registry requiring authentication, you first need to authenticate to the given registry using `docker login <REGISTRY>`.
|
18
|
+
|
19
|
+
## Table of contents
|
20
|
+
|
21
|
+
<!-- MarkdownTOC autolink=true bracket=round depth=4 -->
|
22
|
+
|
23
|
+
- [Usage](#usage)
|
24
|
+
- [`tagfish tags`](#tagfish-tags)
|
25
|
+
- [Example](#example)
|
26
|
+
- [`tagfish search`](#tagfish-search)
|
27
|
+
- [Example](#example-1)
|
28
|
+
- [`tagfish update`](#tagfish-update)
|
29
|
+
- [Example](#example-2)
|
30
|
+
- [Official repositories](#official-repositories)
|
31
|
+
- [Installation](#installation)
|
32
|
+
- [Gem](#gem)
|
33
|
+
- [Limitations](#limitations)
|
34
|
+
- [Docker image](#docker-image)
|
35
|
+
- [Contributing](#contributing)
|
36
|
+
- [Licence](#licence)
|
37
|
+
|
38
|
+
|
39
|
+
<!-- /MarkdownTOC -->
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
The Tagfish CLI tool has different subcommands for interacting with Docker registries.
|
43
|
+
|
44
|
+
### `tagfish tags`
|
45
|
+
The `tags` subcommands is used to retrieve tags from a given repository:
|
46
|
+
|
47
|
+
Usage:
|
48
|
+
tagfish tags [OPTIONS] REPOSITORY
|
49
|
+
|
50
|
+
Parameters:
|
51
|
+
REPOSITORY docker repository
|
52
|
+
|
53
|
+
Options:
|
54
|
+
-l, --latest only return latest explicitly tagged image
|
55
|
+
-s, --short only return tag, not full image path
|
56
|
+
-h, --help print help
|
57
|
+
|
58
|
+
Where `repository` is a docker repository path, including the docker registry. The tags are returned in alphabetical order.
|
59
|
+
|
60
|
+
The `--latest` option gets the image ID of the docker image tagged `latest` in the repository, finds a matching image with a tag set manually (e.g: date, version number), and returns that tag. This option will not work if there is no image tagged `latest` in your repository.
|
61
|
+
|
62
|
+
#### Example
|
63
|
+
```
|
64
|
+
$ tagfish tags alpine
|
65
|
+
alpine:2.6
|
66
|
+
alpine:2.7
|
67
|
+
alpine:3.1
|
68
|
+
alpine:3.2
|
69
|
+
alpine:edge
|
70
|
+
alpine:latest
|
71
|
+
```
|
72
|
+
|
73
|
+
### `tagfish search`
|
74
|
+
The `search` command is used to search for a repository in a given registry.
|
75
|
+
|
76
|
+
Usage:
|
77
|
+
tagfish search [OPTIONS] [KEYWORD]
|
78
|
+
|
79
|
+
Parameters:
|
80
|
+
[KEYWORD] object to search
|
81
|
+
|
82
|
+
Options:
|
83
|
+
-r, --registry REGISTRY Docker registry (default: "index.docker.io")
|
84
|
+
-h, --help print help
|
85
|
+
|
86
|
+
Note: `search` will not work if the search API is disabled on the registry side.
|
87
|
+
|
88
|
+
#### Example
|
89
|
+
```
|
90
|
+
$ tagfish search alpine
|
91
|
+
alpine
|
92
|
+
1science/alpine
|
93
|
+
webhippie/alpine
|
94
|
+
anapsix/alpine-java
|
95
|
+
colstrom/alpine
|
96
|
+
appelgriebsch/alpine
|
97
|
+
[...]
|
98
|
+
```
|
99
|
+
|
100
|
+
### `tagfish update`
|
101
|
+
The `update` subcommand is used to update a file with the latest tags available:
|
102
|
+
|
103
|
+
Usage:
|
104
|
+
tagfish update [OPTIONS] FILE
|
105
|
+
|
106
|
+
Parameters:
|
107
|
+
FILE file to update
|
108
|
+
|
109
|
+
Options:
|
110
|
+
-d, --dry-run enable dry run
|
111
|
+
--only PATTERN Only update repositories matching pattern. Wildcards `*` may be used.
|
112
|
+
-h, --help print help
|
113
|
+
|
114
|
+
#### Example
|
115
|
+
```
|
116
|
+
$ tagfish update --dry-run Dockerfile
|
117
|
+
-FROM docker-registry.delivery.realestate.com.au/gpde/ubuntu-ruby2.2:201508191500
|
118
|
+
+FROM docker-registry.delivery.realestate.com.au/gpde/ubuntu-ruby2.2:201511261833
|
119
|
+
```
|
120
|
+
|
121
|
+
#### Official repositories
|
122
|
+
`tagfish update` will update repositories such as:
|
123
|
+
```
|
124
|
+
private.registry/namespace/repository:tag
|
125
|
+
namespace/repository:tag
|
126
|
+
```
|
127
|
+
However, it will not update the tag of official repositories, such as:
|
128
|
+
```
|
129
|
+
ubuntu:tag
|
130
|
+
```
|
131
|
+
This is because updating to a new OS automatically might be something you want to avoid, and because it is hard to match a repository without a namespace.
|
132
|
+
|
133
|
+
## Installation
|
134
|
+
### Gem
|
135
|
+
Tagfish is packaged as a Ruby gem. Install it from the command line:
|
136
|
+
|
137
|
+
```
|
138
|
+
$ gem install tagfish
|
139
|
+
```
|
140
|
+
|
141
|
+
#### Limitations
|
142
|
+
Tagfish requires Ruby 2.2 or newer if the registry you are accessing restricts TLS to v1.2.
|
143
|
+
|
144
|
+
### Docker image
|
145
|
+
Tagfish is released as a Docker image as well, and can be run with:
|
146
|
+
|
147
|
+
```
|
148
|
+
docker run --rm \
|
149
|
+
-v ~/.docker/config.json:/root/.docker/config.json:ro \
|
150
|
+
-v ${PWD}:/cwd \
|
151
|
+
cowbell/tagfish
|
152
|
+
```
|
153
|
+
|
154
|
+
## Contributing
|
155
|
+
|
156
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/realestate-com-au/tagfish .
|
157
|
+
|
158
|
+
## Licence
|
159
|
+
|
160
|
+
Copyright (c) 2015 REA Group Ltd.
|
161
|
+
|
162
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
163
|
+
of this software and associated documentation files (the "Software"), to deal
|
164
|
+
in the Software without restriction, including without limitation the rights
|
165
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
166
|
+
copies of the Software, and to permit persons to whom the Software is
|
167
|
+
furnished to do so, subject to the following conditions:
|
168
|
+
|
169
|
+
The above copyright notice and this permission notice shall be included in
|
170
|
+
all copies or substantial portions of the Software.
|
171
|
+
|
172
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
173
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
174
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
175
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
176
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
177
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
178
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "fileutils"
|
3
|
+
|
4
|
+
spec = Gem::Specification::load(Dir.glob("*.gemspec").first)
|
5
|
+
gem_file = "pkg/#{spec.name}-#{spec.version}.gem"
|
6
|
+
docker_uri = "cowbell/#{spec.name}"
|
7
|
+
tag = "#{spec.version}"
|
8
|
+
|
9
|
+
desc "build docker image locally"
|
10
|
+
task build_docker_image: [:build] do
|
11
|
+
FileUtils.copy(gem_file, "pkg/tagfish-latest.gem")
|
12
|
+
sh "docker build -t #{docker_uri}:#{tag} ."
|
13
|
+
sh "docker tag -f #{docker_uri}:#{tag} #{docker_uri}:latest"
|
14
|
+
puts "Built image #{docker_uri}"
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "Release docker image"
|
18
|
+
task release_docker_image: [:build_docker_image] do
|
19
|
+
sh "docker push #{docker_uri}:#{tag}"
|
20
|
+
sh "docker push #{docker_uri}:latest"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Check for git sync"
|
24
|
+
task :no_local_changes do
|
25
|
+
sh "git pull"
|
26
|
+
sh "git diff HEAD --exit-code"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Tag version into git repo"
|
30
|
+
task git_tag: [:no_local_changes] do
|
31
|
+
puts "blah"
|
32
|
+
sh "git tag #{tag}"
|
33
|
+
sh "git push origin --tags"
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Release gem and docker image"
|
37
|
+
task release_all: [:git_tag, :release, :release_docker_image] do
|
38
|
+
puts "Released all"
|
39
|
+
end
|
data/bin/tagfish
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
4
|
+
|
5
|
+
require 'clamp'
|
6
|
+
require 'tagfish/tags_command'
|
7
|
+
require 'tagfish/update/update_command'
|
8
|
+
require 'tagfish/search_command'
|
9
|
+
require "tagfish/version"
|
10
|
+
|
11
|
+
module Tagfish
|
12
|
+
class MainCommand < Clamp::Command
|
13
|
+
option ["-v", "--version"], :flag, "display version" do
|
14
|
+
puts "tagfish-#{Tagfish::VERSION}"
|
15
|
+
exit 0
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class MainCommand < Clamp::Command
|
20
|
+
subcommand "tags", "find tags for a repository", TagsCommand
|
21
|
+
end
|
22
|
+
|
23
|
+
class MainCommand < Clamp::Command
|
24
|
+
subcommand "update", "inspect files for outdated dependencies", Update::UpdateCommand
|
25
|
+
end
|
26
|
+
|
27
|
+
class MainCommand < Clamp::Command
|
28
|
+
subcommand "search", "search a registry for repositories", SearchCommand
|
29
|
+
end
|
30
|
+
|
31
|
+
MainCommand.run
|
32
|
+
end
|
data/lib/tagfish.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Tagfish
|
2
|
+
class APICall
|
3
|
+
attr_accessor :uri
|
4
|
+
attr_accessor :http
|
5
|
+
attr_accessor :request
|
6
|
+
|
7
|
+
def initialize(uri_string)
|
8
|
+
@uri = URI.parse(uri_string)
|
9
|
+
@http = Net::HTTP.new(uri.host, uri.port)
|
10
|
+
@http.use_ssl = true if uri.port == 443
|
11
|
+
@request = Net::HTTP::Get.new(uri.request_uri)
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_json(http_auth=nil)
|
15
|
+
begin
|
16
|
+
auth(http_auth) if http_auth
|
17
|
+
response = http.request(request)
|
18
|
+
if response.code == "200"
|
19
|
+
return JSON.parse(response.body)
|
20
|
+
else
|
21
|
+
abort("Call to the registry API failed, the following resource might not exist:\n#{uri.to_s}")
|
22
|
+
end
|
23
|
+
rescue SocketError
|
24
|
+
puts "ERROR: SocketError"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def response_code(http_auth=nil)
|
29
|
+
auth(http_auth) if http_auth
|
30
|
+
begin
|
31
|
+
http.request(request).code.to_i
|
32
|
+
rescue SocketError
|
33
|
+
abort("Call to the registry API failed, the following resource might not exist:\n#{uri.to_s}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def auth(http_auth)
|
38
|
+
@request.basic_auth(http_auth.username, http_auth.password)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'tagfish/docker_uri'
|
4
|
+
require 'tagfish/api_call'
|
5
|
+
|
6
|
+
module Tagfish
|
7
|
+
class DockerAPI
|
8
|
+
|
9
|
+
attr_accessor :docker_uri
|
10
|
+
attr_accessor :api_version
|
11
|
+
attr_accessor :http_auth
|
12
|
+
|
13
|
+
def initialize(docker_uri)
|
14
|
+
@docker_uri = docker_uri
|
15
|
+
retrieve_api_version_and_auth()
|
16
|
+
end
|
17
|
+
|
18
|
+
def retrieve_api_version_and_auth
|
19
|
+
code = try_api('v1')
|
20
|
+
if code != 200
|
21
|
+
code = try_api('v2')
|
22
|
+
end
|
23
|
+
if code == 401
|
24
|
+
abort("Authentication failed, please `docker login <REGISTRY>` and try again.")
|
25
|
+
elsif code != 200
|
26
|
+
abort("API version not recognized")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def try_api(version)
|
31
|
+
code = APICall.new(ping_uri(version)).response_code
|
32
|
+
if code == 200
|
33
|
+
@api_version = version
|
34
|
+
elsif code == 401
|
35
|
+
code = init_auth(version)
|
36
|
+
if code == 200
|
37
|
+
@api_version = version
|
38
|
+
end
|
39
|
+
end
|
40
|
+
return code
|
41
|
+
end
|
42
|
+
|
43
|
+
def init_auth(api_version)
|
44
|
+
@http_auth = DockerHttpAuth.new(docker_uri.registry)
|
45
|
+
if api_version == 'v2'
|
46
|
+
code = APICall.new(ping_v2_uri).response_code(http_auth)
|
47
|
+
elsif api_version == 'v1'
|
48
|
+
code = APICall.new(ping_v1_uri).response_code(http_auth)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def tags_v1
|
53
|
+
APICall.new(tags_v1_uri).get_json(http_auth)
|
54
|
+
end
|
55
|
+
|
56
|
+
def tags_v2
|
57
|
+
APICall.new(tags_v2_uri).get_json(http_auth)
|
58
|
+
end
|
59
|
+
|
60
|
+
def hash_v2(tag)
|
61
|
+
APICall.new(hash_v2_uri(tag)).get_json(http_auth)
|
62
|
+
end
|
63
|
+
|
64
|
+
def catalog_v2
|
65
|
+
APICall.new(catalog_v2_uri).get_json(http_auth)
|
66
|
+
end
|
67
|
+
|
68
|
+
def search_v1(keyword)
|
69
|
+
APICall.new(search_v1_uri(keyword)).get_json(http_auth)
|
70
|
+
end
|
71
|
+
|
72
|
+
def base_uri
|
73
|
+
"#{docker_uri.protocol}#{docker_uri.registry}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def ping_uri(version)
|
77
|
+
if version == 'v1'
|
78
|
+
ping_v1_uri
|
79
|
+
elsif version == 'v2'
|
80
|
+
ping_v2_uri
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def ping_v2_uri
|
85
|
+
"#{base_uri}/v2/"
|
86
|
+
end
|
87
|
+
|
88
|
+
def ping_v1_uri
|
89
|
+
"#{base_uri}/v1/_ping"
|
90
|
+
end
|
91
|
+
|
92
|
+
def catalog_v2_uri
|
93
|
+
"#{base_uri}/v2/_catalog"
|
94
|
+
end
|
95
|
+
|
96
|
+
def search_v1_uri(keyword)
|
97
|
+
"#{base_uri}/v1/search?q=#{keyword}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def tags_v1_uri
|
101
|
+
"#{base_uri}/v1/repositories/#{docker_uri.repository}/tags"
|
102
|
+
end
|
103
|
+
|
104
|
+
def tags_v2_uri
|
105
|
+
"#{base_uri}/v2/#{docker_uri.repository}/tags/list"
|
106
|
+
end
|
107
|
+
|
108
|
+
def hash_v2_uri(tag)
|
109
|
+
"#{base_uri}/v2/#{docker_uri.repository}/manifests/#{tag}"
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Tagfish
|
5
|
+
class DockerHttpAuth
|
6
|
+
|
7
|
+
attr_accessor :username
|
8
|
+
attr_accessor :password
|
9
|
+
|
10
|
+
def initialize(registry)
|
11
|
+
file_path = '~/.docker/config.json'
|
12
|
+
|
13
|
+
begin
|
14
|
+
config = File.open(File.expand_path(file_path), 'r')
|
15
|
+
rescue Exception => e
|
16
|
+
abort("Tried to get a SLiP but the file #{file_path} does not exist")
|
17
|
+
end
|
18
|
+
|
19
|
+
json_config = JSON.parse(config.read())
|
20
|
+
config.close()
|
21
|
+
if json_config['auths'].length == 0
|
22
|
+
@username, @password = nil, nil
|
23
|
+
else
|
24
|
+
b64_auth = json_config['auths'][registry]['auth']
|
25
|
+
auth = Base64.decode64(b64_auth)
|
26
|
+
@username, @password = auth.split(':')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'tagfish/docker_http_auth'
|
2
|
+
require 'tagfish/docker_api'
|
3
|
+
|
4
|
+
module Tagfish
|
5
|
+
class DockerURI
|
6
|
+
URI_PARSER = %r{
|
7
|
+
(https?:\/\/)? # Optional protocol
|
8
|
+
(?:([\w.\-]+\.[\w.\-]+)\/)? # Optional registry
|
9
|
+
([\w\-]*\/?[\w\-.]+) # Optional namespace, mandatory repository
|
10
|
+
:? # Optional delimiter between repository and tag
|
11
|
+
([\w.\-]+)? # Optional tag
|
12
|
+
}x
|
13
|
+
|
14
|
+
def self.parse(docker_string)
|
15
|
+
match = docker_string.match(URI_PARSER)
|
16
|
+
new(*match.captures)
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :protocol
|
20
|
+
attr_accessor :registry
|
21
|
+
attr_accessor :repository
|
22
|
+
attr_accessor :tag
|
23
|
+
|
24
|
+
def initialize(protocol, registry, repository, tag)
|
25
|
+
@protocol = protocol
|
26
|
+
@registry = registry
|
27
|
+
@repository = repository
|
28
|
+
@tag = tag
|
29
|
+
|
30
|
+
if registry.nil?
|
31
|
+
@protocol = "https://"
|
32
|
+
@registry = "index.docker.io"
|
33
|
+
elsif protocol.nil?
|
34
|
+
@protocol = "https://"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def repo_and_tag
|
39
|
+
tag.nil? ? "#{repository}" : "#{repository}:#{tag}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def tag?
|
43
|
+
not tag.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def tagged_latest?
|
47
|
+
tag == 'latest'
|
48
|
+
end
|
49
|
+
|
50
|
+
def with_tag(new_tag)
|
51
|
+
self.class.new(protocol, registry, repository, new_tag)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
reg_sep = registry.nil? ? nil : "/"
|
56
|
+
tag_sep = tag.nil? ? nil : ":"
|
57
|
+
if registry == "index.docker.io"
|
58
|
+
"#{repository}#{tag_sep}#{tag}"
|
59
|
+
else
|
60
|
+
"#{registry}#{reg_sep}#{repository}#{tag_sep}#{tag}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'tagfish/docker_uri'
|
2
|
+
|
3
|
+
module Tagfish
|
4
|
+
class SearchCommand < Clamp::Command
|
5
|
+
parameter "[KEYWORD]", "object to search"
|
6
|
+
option ["-r", "--registry"], "REGISTRY", "Docker registry", :default => "index.docker.io"
|
7
|
+
|
8
|
+
def execute
|
9
|
+
if not registry and not keyword
|
10
|
+
abort("You need to specify a REGISTRY and/or a KEYWORD")
|
11
|
+
end
|
12
|
+
|
13
|
+
docker_uri = DockerURI.parse(registry + "/" + "dummy")
|
14
|
+
docker_api = DockerAPI.new(docker_uri)
|
15
|
+
|
16
|
+
if docker_api.api_version == 'v2'
|
17
|
+
repos = docker_api.catalog_v2["repositories"]
|
18
|
+
if keyword
|
19
|
+
repos.select! {|repo| repo.include? keyword}
|
20
|
+
end
|
21
|
+
repos.each {|repo| puts "#{docker_uri.registry}/#{repo}"}
|
22
|
+
else # 'v1'
|
23
|
+
if not keyword
|
24
|
+
abort("You need to specify a keyword to search a Registry V1")
|
25
|
+
end
|
26
|
+
repos_raw = docker_api.search_v1(keyword)
|
27
|
+
repos = repos_raw["results"].map {|result| result["name"]}
|
28
|
+
puts repos
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/tagfish/tags.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Tagfish
|
5
|
+
class Tags
|
6
|
+
|
7
|
+
def initialize(tags)
|
8
|
+
@tags = tags
|
9
|
+
end
|
10
|
+
|
11
|
+
def tag_names
|
12
|
+
@tags.keys.sort
|
13
|
+
end
|
14
|
+
|
15
|
+
def latest_tag
|
16
|
+
tag_names.select do |tag_name|
|
17
|
+
(@tags[tag_name] == @tags["latest"]) && (tag_name != 'latest')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def latest_tag_to_s
|
22
|
+
latest_tag.empty? ? nil : latest_tag[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "tagfish/tags_logic"
|
2
|
+
require "tagfish/docker_uri"
|
3
|
+
|
4
|
+
module Tagfish
|
5
|
+
class TagsCommand < Clamp::Command
|
6
|
+
parameter "REPOSITORY", "docker repository"
|
7
|
+
option ["-l", "--latest"], :flag, "only return latest explicitly tagged image"
|
8
|
+
option ["-s", "--short"], :flag, "only return tag, not full image path"
|
9
|
+
|
10
|
+
def execute
|
11
|
+
tags_only = latest? ? false : true
|
12
|
+
|
13
|
+
docker_uri = DockerURI.parse(repository)
|
14
|
+
docker_api = DockerAPI.new(docker_uri)
|
15
|
+
tags = TagsLogic.find_tags_by_repository(docker_api, tags_only)
|
16
|
+
|
17
|
+
begin
|
18
|
+
tags_found = latest? ? tags.latest_tag : tags.tag_names
|
19
|
+
rescue Exception => e
|
20
|
+
puts e.message
|
21
|
+
return
|
22
|
+
end
|
23
|
+
|
24
|
+
if tags_found.size == 0
|
25
|
+
puts "ERROR: No image explicitly tagged in this Repository, " +
|
26
|
+
"only `latest` tag available."
|
27
|
+
return
|
28
|
+
end
|
29
|
+
|
30
|
+
pretty_tags = tags_found.map do |tag_name|
|
31
|
+
short? ? tag_name : docker_uri.with_tag(tag_name).to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
puts pretty_tags
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'tagfish/tags'
|
3
|
+
|
4
|
+
module Tagfish
|
5
|
+
class TagsLogic
|
6
|
+
def self.find_tags_by_repository(docker_api, tags_only=false)
|
7
|
+
if docker_api.api_version == 'v2'
|
8
|
+
tags_list = tags_v2(docker_api, tags_only)
|
9
|
+
else
|
10
|
+
tags_list = tags_v1(docker_api)
|
11
|
+
end
|
12
|
+
Tagfish::Tags.new(tags_list)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def self.tags_v1(docker_api)
|
18
|
+
tags_json = docker_api.tags_v1
|
19
|
+
tags_v1_api(tags_json)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.tags_v1_api(api_response_data)
|
23
|
+
case api_response_data
|
24
|
+
when Hash
|
25
|
+
api_response_data
|
26
|
+
when Array
|
27
|
+
api_response_data.reduce({}) do |images, tag|
|
28
|
+
images.merge({tag["name"] => tag["layer"]})
|
29
|
+
end
|
30
|
+
else
|
31
|
+
raise "unexpected type #{api_response_data.class}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.tags_v2(docker_api, tags_only)
|
36
|
+
tags = docker_api.tags_v2["tags"]
|
37
|
+
if tags.nil?
|
38
|
+
abort("No Tags found for this repository")
|
39
|
+
end
|
40
|
+
|
41
|
+
tags_with_hashes = tags.inject({}) do |dict, tag|
|
42
|
+
if tags_only
|
43
|
+
dict[tag] = "dummy_hash"
|
44
|
+
else
|
45
|
+
dict[tag] = docker_api.hash_v2(tag)["fsLayers"][0]["blobSum"]
|
46
|
+
end
|
47
|
+
dict
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Tagfish
|
2
|
+
class Tokeniser
|
3
|
+
|
4
|
+
class Text < String
|
5
|
+
def is_a_uri_token?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class URI < String
|
11
|
+
def is_a_uri_token?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.tokenise(rest)
|
17
|
+
tokens = []
|
18
|
+
while true
|
19
|
+
match = rest.match /[\w\/:.-]+\/[\w.-]+:[\w.-]+/
|
20
|
+
if match.nil?
|
21
|
+
tokens << Text.new(rest)
|
22
|
+
break
|
23
|
+
else
|
24
|
+
tokens << Text.new(match.pre_match)
|
25
|
+
tokens << URI.new(match.to_s)
|
26
|
+
rest = match.post_match
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
tokens
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.dump(tokens)
|
34
|
+
tokens.join('')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'diffy'
|
2
|
+
|
3
|
+
module Tagfish
|
4
|
+
module Update
|
5
|
+
class Differ
|
6
|
+
def self.diff(original_string, updated_string)
|
7
|
+
diffy_diff = Diffy::Diff.new(original_string, updated_string, context: 2)
|
8
|
+
colour_diff = diffy_diff.to_s(:color).chomp
|
9
|
+
colour_diff.empty? ? "Nothing to update here" : colour_diff
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'tagfish/update/updater'
|
2
|
+
require 'tagfish/update/differ'
|
3
|
+
require 'tagfish/update/uri_filters'
|
4
|
+
require 'tagfish/tokeniser'
|
5
|
+
|
6
|
+
module Tagfish
|
7
|
+
module Update
|
8
|
+
class UpdateCommand < Clamp::Command
|
9
|
+
parameter "FILE", "file to update"
|
10
|
+
option ["-d", "--dry-run"], :flag, "enable dry run"
|
11
|
+
option "--only", "PATTERN", "Only update repositories matching pattern. Wildcards (*) may be used."
|
12
|
+
|
13
|
+
def execute
|
14
|
+
filters = [
|
15
|
+
URIFilters.must_be_tagged,
|
16
|
+
URIFilters.must_not_be_tagged_latest,
|
17
|
+
URIFilters.must_match_repository(only)
|
18
|
+
]
|
19
|
+
updater = Updater.new(filters)
|
20
|
+
original = File.read(file)
|
21
|
+
updated = Tokeniser.dump(updater.update(Tokeniser.tokenise(original)))
|
22
|
+
|
23
|
+
puts Differ.diff(original, updated)
|
24
|
+
|
25
|
+
if not dry_run?
|
26
|
+
File.write(file, updated)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'tagfish/tokeniser'
|
2
|
+
require 'tagfish/docker_uri'
|
3
|
+
require 'tagfish/tags_logic'
|
4
|
+
|
5
|
+
module Tagfish
|
6
|
+
module Update
|
7
|
+
class Updater
|
8
|
+
def initialize(filters)
|
9
|
+
@filters = filters
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(tokens)
|
13
|
+
tokens.map do |token|
|
14
|
+
if token.is_a_uri_token?
|
15
|
+
update_uri_token(token)
|
16
|
+
else
|
17
|
+
token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def update_uri_token(token)
|
25
|
+
original_uri = DockerURI.parse(token)
|
26
|
+
if updatable?(original_uri)
|
27
|
+
updated_uri = update_uri(original_uri)
|
28
|
+
Tokeniser::URI.new(updated_uri.to_s)
|
29
|
+
else
|
30
|
+
token
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def updatable?(uri)
|
35
|
+
@filters.all? do |filter|
|
36
|
+
filter.call uri
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_uri(docker_uri)
|
41
|
+
docker_api = DockerAPI.new(docker_uri)
|
42
|
+
tags = TagsLogic.find_tags_by_repository(docker_api)
|
43
|
+
newest_tag_name = tags.latest_tag_to_s
|
44
|
+
if newest_tag_name.nil?
|
45
|
+
docker_uri
|
46
|
+
else
|
47
|
+
docker_uri.with_tag(newest_tag_name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Tagfish
|
2
|
+
module Update
|
3
|
+
class URIFilters
|
4
|
+
def self.must_be_tagged
|
5
|
+
lambda do |uri| uri.tag? end
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.must_not_be_tagged_latest
|
9
|
+
lambda do |uri| not uri.tagged_latest? end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.must_match_repository(pattern)
|
13
|
+
starts_with_pattern = "#{pattern}*"
|
14
|
+
lambda do |uri|
|
15
|
+
File.fnmatch(starts_with_pattern, uri.with_tag(nil).to_s)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/logo.jpg
ADDED
Binary file
|
data/tagfish.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'tagfish/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "tagfish"
|
8
|
+
spec.version = Tagfish::VERSION
|
9
|
+
spec.authors = ["Clement Labbe"]
|
10
|
+
spec.email = ["clement.labbe@rea-group.com"]
|
11
|
+
spec.summary = %q{Command line utility for docker registries}
|
12
|
+
spec.description = "Retrieve repository tags, update dockerfiles, and more!"
|
13
|
+
spec.homepage = "https://github.com/realestate-com-au/tagfish"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "bin"
|
18
|
+
spec.executables = ["tagfish"]
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec", "~> 3.3.0"
|
24
|
+
spec.add_dependency "clamp", "~> 1.0.0"
|
25
|
+
spec.add_dependency "diffy", "~> 3.0.0"
|
26
|
+
spec.add_dependency "json", "~> 1.8.0"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tagfish
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Clement Labbe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.3.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.3.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: clamp
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.0.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: diffy
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.0.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.0.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: json
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.8.0
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.8.0
|
97
|
+
description: Retrieve repository tags, update dockerfiles, and more!
|
98
|
+
email:
|
99
|
+
- clement.labbe@rea-group.com
|
100
|
+
executables:
|
101
|
+
- tagfish
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- Dockerfile
|
107
|
+
- Gemfile
|
108
|
+
- README.md
|
109
|
+
- Rakefile
|
110
|
+
- bin/tagfish
|
111
|
+
- lib/tagfish.rb
|
112
|
+
- lib/tagfish/api_call.rb
|
113
|
+
- lib/tagfish/docker_api.rb
|
114
|
+
- lib/tagfish/docker_http_auth.rb
|
115
|
+
- lib/tagfish/docker_uri.rb
|
116
|
+
- lib/tagfish/search_command.rb
|
117
|
+
- lib/tagfish/tags.rb
|
118
|
+
- lib/tagfish/tags_command.rb
|
119
|
+
- lib/tagfish/tags_logic.rb
|
120
|
+
- lib/tagfish/tokeniser.rb
|
121
|
+
- lib/tagfish/update/differ.rb
|
122
|
+
- lib/tagfish/update/update_command.rb
|
123
|
+
- lib/tagfish/update/updater.rb
|
124
|
+
- lib/tagfish/update/uri_filters.rb
|
125
|
+
- lib/tagfish/version.rb
|
126
|
+
- logo.jpg
|
127
|
+
- tagfish.gemspec
|
128
|
+
homepage: https://github.com/realestate-com-au/tagfish
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.0.14
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: Command line utility for docker registries
|
152
|
+
test_files: []
|