docker_distribution 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +108 -0
- data/Rakefile +16 -0
- data/lib/docker_distribution/canonical_reference.rb +32 -0
- data/lib/docker_distribution/digest.rb +50 -0
- data/lib/docker_distribution/digest_reference.rb +19 -0
- data/lib/docker_distribution/digest_set.rb +83 -0
- data/lib/docker_distribution/errors.rb +30 -0
- data/lib/docker_distribution/helpers.rb +62 -0
- data/lib/docker_distribution/normalize.rb +125 -0
- data/lib/docker_distribution/reference.rb +178 -0
- data/lib/docker_distribution/regexp.rb +226 -0
- data/lib/docker_distribution/repository.rb +34 -0
- data/lib/docker_distribution/tagged_reference.rb +32 -0
- data/lib/docker_distribution/version.rb +5 -0
- data/lib/docker_distribution.rb +47 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df29a563df5c3905dee99b74d6b1dfef79175c73a27ad016e553391cfa026ed7
|
4
|
+
data.tar.gz: c4bcaf72d7525f20042b5cc91a22e6f8ac18692cd31f7744dfca6d0012929201
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ab3c059d0149abaf42364f194b37923e33316fae3e5395991f8ecf9a462ac69245387cea1dcbafa58ad043e05202cca32cea3e580e67f1476a1b67b6fae6d3c
|
7
|
+
data.tar.gz: dce9c44d2c73b1b5982d1909de563227c21f8e2d499916f3d29cfb28d6c8a0796c189107131601ae7d0a6217d5bb4a0294266d9b3cce160ac917cd1e2b77d6b0
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# DockerDistribution
|
2
|
+
|
3
|
+
Implementation of OCI Distribution Specification on Ruby. Implementation copied from Golang version which used in Docker Cli and Kubectl Cli.
|
4
|
+
|
5
|
+
[OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
|
6
|
+
|
7
|
+
[Golang implementation](https://github.com/docker/distribution/tree/main/reference)
|
8
|
+
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
```
|
13
|
+
gem install docker_distribution
|
14
|
+
```
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
### Reference
|
19
|
+
* Add description from implementation comments
|
20
|
+
```
|
21
|
+
DockerDistribution::Reference.parse(image_name)
|
22
|
+
DockerDistribution::Reference.parse_named(str)
|
23
|
+
DockerDistribution::Reference.with_name(name)
|
24
|
+
DockerDistribution::Reference.with_tag(named, tag)
|
25
|
+
DockerDistribution::Reference.with_digest(named, digest)
|
26
|
+
DockerDistribution::Reference.split_domain(name)
|
27
|
+
DockerDistribution::Reference.split_hostname(named)
|
28
|
+
```
|
29
|
+
|
30
|
+
### Normalize
|
31
|
+
* Add description from implementation comments
|
32
|
+
```
|
33
|
+
DockerDistribution::Normalize.parse_normalized_named(str)
|
34
|
+
DockerDistribution::Normalize.parse_docker_ref(ref)
|
35
|
+
DockerDistribution::Normalize.split_docker_domain(name)
|
36
|
+
DockerDistribution::Normalize.familiarize_name(repo)
|
37
|
+
DockerDistribution::Normalize.tag_name_only(ref)
|
38
|
+
DockerDistribution::Normalize.parse_any_reference(ref)
|
39
|
+
DockerDistribution::Normalize.parse_any_reference_with_set(ref, digest_set)
|
40
|
+
```
|
41
|
+
|
42
|
+
### Regexp
|
43
|
+
* Add description from implementation comments
|
44
|
+
```
|
45
|
+
DockerDistribution::Regexp.alpha_numeric
|
46
|
+
DockerDistribution::Regexp.separator
|
47
|
+
DockerDistribution::Regexp.name_component
|
48
|
+
DockerDistribution::Regexp.domain_name_component
|
49
|
+
DockerDistribution::Regexp.ipv6address
|
50
|
+
DockerDistribution::Regexp.domain_name
|
51
|
+
DockerDistribution::Regexp.host
|
52
|
+
DockerDistribution::Regexp.domain
|
53
|
+
DockerDistribution::Regexp.domain_regexp
|
54
|
+
DockerDistribution::Regexp.tag
|
55
|
+
DockerDistribution::Regexp.tag_regexp
|
56
|
+
DockerDistribution::Regexp.anchored_tag
|
57
|
+
DockerDistribution::Regexp.anchored_tag_regexp
|
58
|
+
DockerDistribution::Regexp.digest_pat
|
59
|
+
DockerDistribution::Regexp.digest_regexp
|
60
|
+
DockerDistribution::Regexp.anchored_digest
|
61
|
+
DockerDistribution::Regexp.anchored_digest_regexp
|
62
|
+
DockerDistribution::Regexp.name_pat
|
63
|
+
DockerDistribution::Regexp.name_regexp
|
64
|
+
DockerDistribution::Regexp.anchored_name
|
65
|
+
DockerDistribution::Regexp.anchored_name_regexp
|
66
|
+
DockerDistribution::Regexp.reference_pat
|
67
|
+
DockerDistribution::Regexp.reference_regexp
|
68
|
+
DockerDistribution::Regexp.identifier
|
69
|
+
DockerDistribution::Regexp.identifier_regexp
|
70
|
+
DockerDistribution::Regexp.short_identifier
|
71
|
+
DockerDistribution::Regexp.short_identifier_regexp
|
72
|
+
DockerDistribution::Regexp.anchored_identifier
|
73
|
+
DockerDistribution::Regexp.anchored_identifier_regexp
|
74
|
+
DockerDistribution::Regexp.anchored_short_identifier
|
75
|
+
DockerDistribution::Regexp.anchored_short_identifier_regexp
|
76
|
+
```
|
77
|
+
|
78
|
+
### Digest
|
79
|
+
* Add description from implementation comments
|
80
|
+
```
|
81
|
+
DockerDistribution::Digest.parse(digest_string)
|
82
|
+
```
|
83
|
+
|
84
|
+
## Examples
|
85
|
+
...
|
86
|
+
|
87
|
+
More examples in tests
|
88
|
+
|
89
|
+
## Development
|
90
|
+
```
|
91
|
+
docker-compose build gem
|
92
|
+
docker-compose run --rm gem bash
|
93
|
+
bundle install
|
94
|
+
...
|
95
|
+
make test
|
96
|
+
make lint
|
97
|
+
```
|
98
|
+
|
99
|
+
## Release
|
100
|
+
1. [Read this](https://guides.rubygems.org/publishing/)
|
101
|
+
2. Make sure you logged in into Rubygems
|
102
|
+
3. Update version accoring [semver](https://semver.org/)
|
103
|
+
4. `gem build docker_distribution.gemspec`
|
104
|
+
5. `gem push docker_distribution-[version].gem`
|
105
|
+
|
106
|
+
## Todo
|
107
|
+
- Finish readme description / examples / installation guide / development guide / examples / ruby version support
|
108
|
+
- Add correct workflow in github to run tests and linters in docker env
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << "test"
|
8
|
+
t.libs << "lib"
|
9
|
+
t.test_files = FileList["test/**/test_*.rb"]
|
10
|
+
end
|
11
|
+
|
12
|
+
require "rubocop/rake_task"
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
task default: %i[test rubocop]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class CanonicalReference
|
5
|
+
extend ::Forwardable
|
6
|
+
def_delegators :repository, :name, :domain, :path
|
7
|
+
|
8
|
+
attr_accessor :repository, :digest
|
9
|
+
|
10
|
+
def initialize(repository, digest)
|
11
|
+
@repository = repository
|
12
|
+
@digest = digest
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
[repository.name, digest].join("@")
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
{
|
21
|
+
repository: name,
|
22
|
+
domain: domain,
|
23
|
+
path: path,
|
24
|
+
digest: digest
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def familiar
|
29
|
+
self.class.new(Normalize.familiarize_name(self), digest)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module DockerDistribution
|
6
|
+
class Digest
|
7
|
+
DEFAULT_ALGORITHM = "sha256"
|
8
|
+
attr_accessor :digest_string, :encoded, :algorithm_type
|
9
|
+
|
10
|
+
# Parse parses digest_string and returns the validated digest object. An error will
|
11
|
+
# be raised if the format is invalid.
|
12
|
+
def self.parse!(digest_string)
|
13
|
+
dgst = new(digest_string)
|
14
|
+
dgst.validate!
|
15
|
+
dgst
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(digest_string)
|
19
|
+
@digest_string = digest_string
|
20
|
+
@algorithm_type, @encoded = digest_string.index(":") ? digest_string.split(":") : [DEFAULT_ALGORITHM, digest_string]
|
21
|
+
end
|
22
|
+
|
23
|
+
def digest
|
24
|
+
[algorithm_type, encoded].compact.join(":")
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
digest
|
29
|
+
end
|
30
|
+
|
31
|
+
# rubocop:disable Metrics/AbcSize
|
32
|
+
def validate!
|
33
|
+
index = digest_string.index(":")
|
34
|
+
raise DigestInvalidFormat if index.nil? || index + 1 == digest_string.length
|
35
|
+
|
36
|
+
raise DigestInvalidLength if algorithm.block_length != encoded.length
|
37
|
+
|
38
|
+
return true if Regexp.anchored_encoded_regexp(algorithm_type.upcase).match?(encoded)
|
39
|
+
|
40
|
+
raise DigestInvalidFormat
|
41
|
+
end
|
42
|
+
# rubocop:enable Metrics/AbcSize
|
43
|
+
|
44
|
+
def algorithm
|
45
|
+
send(:Digest, algorithm_type.upcase).new
|
46
|
+
rescue NameError, LoadError
|
47
|
+
raise DigestUnsupported
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class DigestReference
|
5
|
+
attr_accessor :digest
|
6
|
+
|
7
|
+
def initialize(digest)
|
8
|
+
@digest = digest
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
digest
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_h
|
16
|
+
{ digest: digest }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module DockerDistribution
|
6
|
+
class DigestSet
|
7
|
+
attr_writer :entries
|
8
|
+
|
9
|
+
def initialize(digest_strings)
|
10
|
+
@entries = digest_strings.map { |digest_string| Digest.new(digest_string) }
|
11
|
+
end
|
12
|
+
|
13
|
+
def entries
|
14
|
+
@entries ||= []
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(digest_string)
|
18
|
+
@entries << Digest.new(digest_string)
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup!(digest_string)
|
22
|
+
raise DigestNotFound if entries.length.zero?
|
23
|
+
|
24
|
+
search_func, encoded, alg = parse!(digest_string)
|
25
|
+
|
26
|
+
entry = search(search_func, encoded, alg)
|
27
|
+
entry.digest
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
33
|
+
def parse!(digest_string)
|
34
|
+
dgst = Digest.parse!(digest_string)
|
35
|
+
alg = dgst.algorithm_type
|
36
|
+
encoded = dgst.encoded
|
37
|
+
search_func = lambda do |i|
|
38
|
+
return entries[i].algorithm_type >= dgst.algorithm_type if entries[i].encoded == encoded
|
39
|
+
|
40
|
+
entries[i].encoded >= encoded
|
41
|
+
end
|
42
|
+
[search_func, encoded, alg]
|
43
|
+
rescue DigestInvalidFormat
|
44
|
+
alg = Digest::DEFAULT_ALGORITHM
|
45
|
+
encoded = digest_string
|
46
|
+
search_func = lambda do |i|
|
47
|
+
entries[i].encoded >= digest_string
|
48
|
+
end
|
49
|
+
[search_func, encoded, alg]
|
50
|
+
end
|
51
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
52
|
+
|
53
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
54
|
+
def search(search_func, encoded, alg)
|
55
|
+
idx = entries.each_with_index.filter_map { |_el, i| i if search_func.call(i) }.first || entries.length
|
56
|
+
raise DigestNotFound if idx == entries.length || !check_short_match(entries[idx].algorithm_type, entries[idx].encoded, alg.to_s,
|
57
|
+
encoded)
|
58
|
+
return entries[idx] if entries[idx].algorithm_type == alg && entries[idx].encoded == encoded
|
59
|
+
|
60
|
+
raise DigestAmbiguous if idx + 1 < entries.length && check_short_match(entries[idx + 1].algorithm_type, entries[idx + 1].encoded,
|
61
|
+
alg.to_s, encoded)
|
62
|
+
|
63
|
+
entries[idx]
|
64
|
+
end
|
65
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
66
|
+
|
67
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
68
|
+
def check_short_match(alg, encoded, short_alg, short_encoded)
|
69
|
+
if encoded.length == short_encoded.length
|
70
|
+
return false if encoded != short_encoded
|
71
|
+
|
72
|
+
return false if short_alg.length.positive? && alg != short_alg
|
73
|
+
end
|
74
|
+
|
75
|
+
return false unless encoded.start_with?(short_encoded)
|
76
|
+
|
77
|
+
return false if short_alg.length.positive? && alg != short_alg
|
78
|
+
|
79
|
+
true
|
80
|
+
end
|
81
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
# NameEmpty is raised for empty, invalid repository names.
|
5
|
+
class NameEmpty < StandardError; end
|
6
|
+
# ReferenceInvalidFormat represents an error while trying to parse a string as a reference.
|
7
|
+
class ReferenceInvalidFormat < StandardError; end
|
8
|
+
# TagInvalidFormat represents an error while trying to parse a string as a tag.
|
9
|
+
class TagInvalidFormat < StandardError; end
|
10
|
+
# DigestError raised when something wrong with Digest
|
11
|
+
class DigestError < StandardError; end
|
12
|
+
# DigestInvalidLength raised when digest has invalid length.
|
13
|
+
class DigestInvalidLength < DigestError; end
|
14
|
+
# DigestInvalidFormat raised when digest format invalid.
|
15
|
+
class DigestInvalidFormat < DigestError; end
|
16
|
+
# DigestUnsupported raised when the digest algorithm is unsupported.
|
17
|
+
class DigestUnsupported < DigestError; end
|
18
|
+
# DigestNotFound raised when the digest set not found
|
19
|
+
class DigestNotFound < DigestError; end
|
20
|
+
# DigestAmbiguous raised when the digest set has two same digests
|
21
|
+
class DigestAmbiguous < DigestError; end
|
22
|
+
# NameContainsUppercase is raised for invalid repository names that contain uppercase characters.
|
23
|
+
class NameContainsUppercase < StandardError; end
|
24
|
+
# NameTooLong is raised when a repository name is longer than 255 symbols.
|
25
|
+
class NameTooLong < StandardError; end
|
26
|
+
# NameNotCanonical is raised when a name is not canonical.
|
27
|
+
class NameNotCanonical < StandardError; end
|
28
|
+
# # ParseNormalizedNamedError is raised when a parse normalized error failed
|
29
|
+
class ParseNormalizedNamedError < StandardError; end
|
30
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class Helpers
|
5
|
+
class << self
|
6
|
+
def familiar_name(named)
|
7
|
+
return named.familiar.name if named.respond_to?(:familiar)
|
8
|
+
|
9
|
+
named.name
|
10
|
+
end
|
11
|
+
|
12
|
+
def familiar_string(named)
|
13
|
+
return named.familiar.to_s if named.respond_to?(:familiar)
|
14
|
+
|
15
|
+
named.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
def familiar_match(pattern, ref)
|
19
|
+
string_match = File.fnmatch(pattern, familiar_string(ref), File::FNM_PATHNAME)
|
20
|
+
name_match = File.fnmatch(pattern, familiar_name(ref), File::FNM_PATHNAME)
|
21
|
+
|
22
|
+
string_match || name_match
|
23
|
+
end
|
24
|
+
|
25
|
+
def tagged?(ref)
|
26
|
+
ref.respond_to?(:tag)
|
27
|
+
end
|
28
|
+
|
29
|
+
def canonical?(ref)
|
30
|
+
ref.respond_to?(:digest)
|
31
|
+
end
|
32
|
+
|
33
|
+
def named?(ref)
|
34
|
+
ref.respond_to?(:name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def familiar?(ref)
|
38
|
+
ref.respond_to?(:familiar)
|
39
|
+
end
|
40
|
+
|
41
|
+
def name_only?(ref)
|
42
|
+
named?(ref) && !tagged?(ref) && !canonical?(ref)
|
43
|
+
end
|
44
|
+
|
45
|
+
def normalized_named?(ref)
|
46
|
+
named?(ref) && familiar?(ref)
|
47
|
+
end
|
48
|
+
|
49
|
+
def from(str, index)
|
50
|
+
str[index..]
|
51
|
+
end
|
52
|
+
|
53
|
+
def to(str, index)
|
54
|
+
str[0...index]
|
55
|
+
end
|
56
|
+
|
57
|
+
def empty?(object)
|
58
|
+
object == "" || object.nil?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class Normalize
|
5
|
+
LEGACY_DEFAULT_DOMAIN = "index.docker.io"
|
6
|
+
DEFAULT_DOMAIN = "docker.io"
|
7
|
+
OFFICIAL_REPO_NAME = "library"
|
8
|
+
DEFAULT_TAG = "latest"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# ParseNormalizedNamed parses a string into a named reference
|
12
|
+
# transforming a familiar name from Docker UI to a fully
|
13
|
+
# qualified reference. If the value may be an identifier
|
14
|
+
# use ParseAnyReference.
|
15
|
+
def parse_normalized_named(str)
|
16
|
+
raise ParseNormalizedNamedError if Regexp.anchored_identifier_regexp.match?(str)
|
17
|
+
|
18
|
+
domain, remainder = split_docker_domain(str)
|
19
|
+
|
20
|
+
tag_sep = remainder.index(":") || -1
|
21
|
+
remote_name = tag_sep > -1 ? Helpers.to(remainder, tag_sep) : remainder
|
22
|
+
raise ParseNormalizedNamedError if remote_name != remote_name.downcase
|
23
|
+
|
24
|
+
ref = DockerDistribution::Reference.parse("#{domain}/#{remainder}")
|
25
|
+
raise ParseNormalizedNamedError if Helpers.empty?(ref)
|
26
|
+
|
27
|
+
ref
|
28
|
+
end
|
29
|
+
|
30
|
+
# ParseDockerRef normalizes the image reference following the docker convention. This is added
|
31
|
+
# mainly for backward compatibility.
|
32
|
+
# The reference returned can only be either tagged or digested. For reference contains both tag
|
33
|
+
# and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
|
34
|
+
# sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
|
35
|
+
# docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
|
36
|
+
def parse_docker_ref(ref)
|
37
|
+
named = parse_normalized_named(ref)
|
38
|
+
if Helpers.tagged?(named) && Helpers.canonical?(named)
|
39
|
+
new_named = Reference.with_name(named.name)
|
40
|
+
new_canonical = Reference.with_digest(new_named, named.digest)
|
41
|
+
return new_canonical
|
42
|
+
end
|
43
|
+
|
44
|
+
tag_name_only(named)
|
45
|
+
end
|
46
|
+
|
47
|
+
# splitDockerDomain splits a repository name to domain and remote name string.
|
48
|
+
# If no valid domain is found, the default domain is used. Repository name
|
49
|
+
# needs to be already validated before.
|
50
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
51
|
+
def split_docker_domain(name)
|
52
|
+
i = name.index("/") || -1
|
53
|
+
|
54
|
+
start_part = Helpers.to(name, i)
|
55
|
+
end_part = Helpers.from(name, i + 1)
|
56
|
+
|
57
|
+
if (i == -1 || [".", ":"].none? do |sep|
|
58
|
+
start_part.include?(sep)
|
59
|
+
end) && start_part != "localhost" && start_part.downcase == start_part
|
60
|
+
domain = DEFAULT_DOMAIN
|
61
|
+
remainder = name
|
62
|
+
else
|
63
|
+
domain = start_part
|
64
|
+
remainder = end_part
|
65
|
+
end
|
66
|
+
|
67
|
+
domain = DEFAULT_DOMAIN if domain == LEGACY_DEFAULT_DOMAIN
|
68
|
+
remainder = [OFFICIAL_REPO_NAME, remainder].join("/") if domain == DEFAULT_DOMAIN && !remainder.include?("/")
|
69
|
+
|
70
|
+
[domain, remainder]
|
71
|
+
end
|
72
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
73
|
+
|
74
|
+
# familiarizeName returns a shortened version of the name familiar
|
75
|
+
# to to the Docker UI. Familiar names have the default domain
|
76
|
+
# "docker.io" and "library/" repository prefix removed.
|
77
|
+
# For example, "docker.io/library/redis" will have the familiar
|
78
|
+
# name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
|
79
|
+
# Returns a familiarized named only reference.
|
80
|
+
def familiarize_name(repo)
|
81
|
+
familiar_repo = Repository.new(repo.domain, repo.path)
|
82
|
+
|
83
|
+
if familiar_repo.domain == DEFAULT_DOMAIN
|
84
|
+
familiar_repo.domain = nil
|
85
|
+
parts = familiar_repo.path.split("/")
|
86
|
+
familiar_repo.path = parts[1] if parts.length == 2 && parts[0] == OFFICIAL_REPO_NAME
|
87
|
+
end
|
88
|
+
familiar_repo
|
89
|
+
end
|
90
|
+
|
91
|
+
# TagNameOnly adds the default tag "latest" to a reference if it only has
|
92
|
+
# a repo name.
|
93
|
+
def tag_name_only(ref)
|
94
|
+
return Reference.with_tag(ref, DEFAULT_TAG) if Helpers.name_only?(ref)
|
95
|
+
|
96
|
+
ref
|
97
|
+
end
|
98
|
+
|
99
|
+
# ParseAnyReference parses a reference string as a possible identifier,
|
100
|
+
# full digest, or familiar name.
|
101
|
+
def parse_any_reference(ref)
|
102
|
+
return DigestReference.new("sha256:#{ref}") if Regexp.anchored_identifier_regexp.match?(ref)
|
103
|
+
|
104
|
+
digest = Digest.parse!(ref)
|
105
|
+
DigestReference.new(digest.digest)
|
106
|
+
rescue DigestError
|
107
|
+
parse_normalized_named(ref)
|
108
|
+
end
|
109
|
+
|
110
|
+
# ParseAnyReferenceWithSet parses a reference string as a possible short
|
111
|
+
# identifier to be matched in a digest set, a full digest, or familiar name.
|
112
|
+
def parse_any_reference_with_set(ref, digest_set)
|
113
|
+
if Regexp.anchored_short_identifier_regexp.match?(ref)
|
114
|
+
dgst = digest_set.lookup!(ref)
|
115
|
+
return DigestReference.new(dgst) if dgst
|
116
|
+
else
|
117
|
+
dgst = Digest.parse!(ref)
|
118
|
+
DigestReference.new(dgst.digest)
|
119
|
+
end
|
120
|
+
rescue DigestError
|
121
|
+
parse_normalized_named(ref)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Naming/MethodParameterName
|
4
|
+
module DockerDistribution
|
5
|
+
class Reference
|
6
|
+
MAX_NAME_TOTAL_LENGTH = 255
|
7
|
+
extend ::Forwardable
|
8
|
+
def_delegators :repository, :name, :domain, :path
|
9
|
+
|
10
|
+
attr_accessor :repository, :tag, :digest
|
11
|
+
|
12
|
+
def initialize(repository = nil, tag = nil, digest = nil)
|
13
|
+
@repository = repository
|
14
|
+
@tag = tag
|
15
|
+
@digest = digest
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
[name, ":", tag, "@", digest].join("")
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
repository: name,
|
25
|
+
domain: domain,
|
26
|
+
path: path,
|
27
|
+
tag: tag,
|
28
|
+
digest: digest
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def familiar
|
33
|
+
self.class.new(Normalize.familiarize_name(self), tag, digest)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parse parses s and returns a syntactically valid Reference.
|
37
|
+
# If an error was encountered it is returned, along with a nil Reference.
|
38
|
+
# NOTE: Parse will not handle short digests.
|
39
|
+
def self.parse(s)
|
40
|
+
matches = Regexp.reference_regexp.match(s)
|
41
|
+
if matches.nil?
|
42
|
+
raise NameEmpty if Helpers.empty?(s)
|
43
|
+
raise NameContainsUppercase unless Regexp.reference_regexp.match(s.downcase).nil?
|
44
|
+
|
45
|
+
raise ReferenceInvalidFormat
|
46
|
+
end
|
47
|
+
|
48
|
+
match_results = matches.to_a
|
49
|
+
raise NameTooLong if match_results[1] && match_results[1].length > MAX_NAME_TOTAL_LENGTH
|
50
|
+
|
51
|
+
repo = Repository.new
|
52
|
+
name_match = Regexp.anchored_name_regexp.match(match_results[1])
|
53
|
+
name_match_results = name_match&.to_a
|
54
|
+
|
55
|
+
if name_match_results && name_match_results.length == 3
|
56
|
+
repo.domain = name_match_results[1]
|
57
|
+
repo.path = name_match_results[2]
|
58
|
+
else
|
59
|
+
repo.domain = nil
|
60
|
+
repo.path = name_match_results[1]
|
61
|
+
end
|
62
|
+
|
63
|
+
ref = Reference.new(repo, match_results[2])
|
64
|
+
|
65
|
+
unless Helpers.empty?(match_results[3])
|
66
|
+
digest = Digest.parse!(match_results[3])
|
67
|
+
ref.digest = digest.digest
|
68
|
+
end
|
69
|
+
|
70
|
+
r = get_best_reference_type(ref)
|
71
|
+
raise NameEmpty if r.nil?
|
72
|
+
|
73
|
+
r
|
74
|
+
end
|
75
|
+
|
76
|
+
# ParseNamed parses s and returns a syntactically valid reference implementing
|
77
|
+
# the Named interface. The reference must have a name and be in the canonical
|
78
|
+
# form, otherwise an error is returned.
|
79
|
+
# If an error was encountered it is returned, along with a nil Reference.
|
80
|
+
# NOTE: ParseNamed will not handle short digests.
|
81
|
+
def self.parse_named(str)
|
82
|
+
named = Normalize.parse_normalized_named(str)
|
83
|
+
raise NameNotCanonical if named.to_s != str
|
84
|
+
|
85
|
+
named
|
86
|
+
end
|
87
|
+
|
88
|
+
# WithName returns a named object representing the given string. If the input
|
89
|
+
# is invalid ErrReferenceInvalidFormat will be returned.
|
90
|
+
def self.with_name(name)
|
91
|
+
raise NameEmpty if Helpers.empty?(name)
|
92
|
+
raise NameTooLong if name.length > MAX_NAME_TOTAL_LENGTH
|
93
|
+
|
94
|
+
match = Regexp.anchored_name_regexp.match(name)
|
95
|
+
raise ReferenceInvalidFormat if match.nil? || match.to_a.length != 3
|
96
|
+
|
97
|
+
match_result = match.to_a
|
98
|
+
Repository.new(match_result[1], match_result[2])
|
99
|
+
end
|
100
|
+
|
101
|
+
# WithTag combines the name from "name" and the tag from "tag" to form a
|
102
|
+
# reference incorporating both the name and the tag.
|
103
|
+
def self.with_tag(named, tag)
|
104
|
+
match = Regexp.anchored_tag_regexp.match(tag)
|
105
|
+
raise TagInvalidFormat if match.nil?
|
106
|
+
|
107
|
+
repo = Repository.new
|
108
|
+
if named.is_a?(Repository)
|
109
|
+
repo.domain = named.domain
|
110
|
+
repo.path = named.path
|
111
|
+
else
|
112
|
+
repo.path = named.name
|
113
|
+
end
|
114
|
+
|
115
|
+
return Reference.new(repo, tag, named.digest) if named.is_a?(CanonicalReference)
|
116
|
+
|
117
|
+
TaggedReference.new(repo, tag)
|
118
|
+
end
|
119
|
+
|
120
|
+
# WithDigest combines the name from "name" and the digest from "digest" to form
|
121
|
+
# a reference incorporating both the name and the digest.
|
122
|
+
def self.with_digest(named, digest)
|
123
|
+
match = Regexp.anchored_digest_regexp.match(digest)
|
124
|
+
raise DigestInvalidFormat if match.nil?
|
125
|
+
|
126
|
+
repo = Repository.new
|
127
|
+
if named.is_a?(Repository)
|
128
|
+
repo.domain = named.domain
|
129
|
+
repo.path = named.path
|
130
|
+
else
|
131
|
+
repo.path = named.name
|
132
|
+
end
|
133
|
+
|
134
|
+
return Reference.new(repo, named.tag, digest) if named.is_a?(TaggedReference)
|
135
|
+
|
136
|
+
CanonicalReference.new(repo, digest)
|
137
|
+
end
|
138
|
+
|
139
|
+
# TrimNamed removes any tag or digest from the named reference.
|
140
|
+
def self.trim_named(ref)
|
141
|
+
domain, path = split_hostname(ref)
|
142
|
+
new Repository(domain, path)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.get_best_reference_type(ref)
|
146
|
+
if Helpers.empty?(ref.name)
|
147
|
+
return DigestReference.new(ref.digest) unless Helpers.empty?(ref.digest)
|
148
|
+
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
|
152
|
+
if Helpers.empty?(ref.tag)
|
153
|
+
return CanonicalReference.new(ref.repository, ref.digest) unless Helpers.empty?(ref.digest)
|
154
|
+
|
155
|
+
return ref.repository
|
156
|
+
end
|
157
|
+
|
158
|
+
return TaggedReference.new(ref.repository, ref.tag) if Helpers.empty?(ref.digest)
|
159
|
+
|
160
|
+
ref
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.split_domain(name)
|
164
|
+
match = Regexp.anchored_name_regexp.match(name)
|
165
|
+
return [nil, name] if match && match.to_a.length != 3
|
166
|
+
|
167
|
+
match_results = match.to_a
|
168
|
+
[match_results[1], match_results[2]]
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.split_hostname(named)
|
172
|
+
return [named.domain, named.path] if named.is_a?(Repository)
|
173
|
+
|
174
|
+
split_domain(name)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
# rubocop:enable all
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
|
+
module DockerDistribution
|
5
|
+
# Representation of regxexps we will use
|
6
|
+
class Regexp
|
7
|
+
class << self
|
8
|
+
# alphaNumeric defines the alpha numeric atom, typically a
|
9
|
+
# component of names. This only allows lower case characters and digits.
|
10
|
+
def alpha_numeric
|
11
|
+
"[a-z0-9]+"
|
12
|
+
end
|
13
|
+
|
14
|
+
# separator defines the separators allowed to be embedded in name
|
15
|
+
# components. This allow one period, one or two underscore and multiple
|
16
|
+
# dashes. Repeated dashes and underscores are intentionally treated
|
17
|
+
# differently. In order to support valid hostnames as name components,
|
18
|
+
# supporting repeated dash was added. Additionally double underscore is
|
19
|
+
# now allowed as a separator to loosen the restriction for previously
|
20
|
+
# supported names.
|
21
|
+
def separator
|
22
|
+
"(?:[._]|__|[-]*)"
|
23
|
+
end
|
24
|
+
|
25
|
+
# nameComponent restricts registry path component names to start
|
26
|
+
# with at least one letter or number, with following parts able to be
|
27
|
+
# separated by one period, one or two underscore and multiple dashes.
|
28
|
+
def name_component
|
29
|
+
expression(alpha_numeric, repeated(separator, alpha_numeric))
|
30
|
+
end
|
31
|
+
|
32
|
+
# domainNameComponent restricts the registry domain component of a
|
33
|
+
# repository name to start with a component as defined by DomainRegexp.
|
34
|
+
def domain_name_component
|
35
|
+
"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"
|
36
|
+
end
|
37
|
+
|
38
|
+
# ipv6address are enclosed between square brackets and may be represented
|
39
|
+
# in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
|
40
|
+
# are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
|
41
|
+
# IPv4-Mapped are deliberately excluded.
|
42
|
+
def ipv6address
|
43
|
+
expression(literal("["), "(?:[a-fA-F0-9:]+)", literal("]"))
|
44
|
+
end
|
45
|
+
|
46
|
+
# domainName defines the structure of potential domain components
|
47
|
+
# that may be part of image names. This is purposely a subset of what is
|
48
|
+
# allowed by DNS to ensure backwards compatibility with Docker image
|
49
|
+
# names. This includes IPv4 addresses on decimal format.
|
50
|
+
def domain_name
|
51
|
+
expression(domain_name_component, repeated(literal("."), domain_name_component))
|
52
|
+
end
|
53
|
+
|
54
|
+
# host defines the structure of potential domains based on the URI
|
55
|
+
# Host subcomponent on rfc3986. It may be a subset of DNS domain name,
|
56
|
+
# or an IPv4 address in decimal format, or an IPv6 address between square
|
57
|
+
# brackets (excluding zone identifiers as defined by rfc6874 or special
|
58
|
+
# addresses such as IPv4-Mapped).
|
59
|
+
def host
|
60
|
+
"(?:#{domain_name}|#{ipv6address})"
|
61
|
+
end
|
62
|
+
|
63
|
+
# allowed by the URI Host subcomponent on rfc3986 to ensure backwards
|
64
|
+
# compatibility with Docker image names.
|
65
|
+
def domain
|
66
|
+
expression(host, optional(literal(":"), "[0-9]+"))
|
67
|
+
end
|
68
|
+
|
69
|
+
# DomainRegexp defines the structure of potential domain components that may be part of image names.
|
70
|
+
# This is purposely a subset of what is allowed by DNS to ensure backwards compatibility with Docker image names.
|
71
|
+
def domain_regexp
|
72
|
+
@domain_regexp ||= ::Regexp.new(domain)
|
73
|
+
end
|
74
|
+
|
75
|
+
def tag
|
76
|
+
"[\\w][\\w.-]{0,127}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# TagRegexp matches valid tag names
|
80
|
+
def tag_regexp
|
81
|
+
::Regexp.new(tag)
|
82
|
+
end
|
83
|
+
|
84
|
+
def anchored_tag
|
85
|
+
anchored(tag)
|
86
|
+
end
|
87
|
+
|
88
|
+
# anchoredTagRegexp matches valid tag names, anchored at the start and end of the matched string.
|
89
|
+
def anchored_tag_regexp
|
90
|
+
::Regexp.new(anchored_tag)
|
91
|
+
end
|
92
|
+
|
93
|
+
def digest_pat
|
94
|
+
"[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}"
|
95
|
+
end
|
96
|
+
|
97
|
+
# DigestRegexp matches valid digests.
|
98
|
+
def digest_regexp
|
99
|
+
::Regexp.new(digest_pat)
|
100
|
+
end
|
101
|
+
|
102
|
+
def anchored_digest
|
103
|
+
anchored(digest_pat)
|
104
|
+
end
|
105
|
+
|
106
|
+
# anchoredDigestRegexp matches valid digests, anchored at the start and end of the matched string.
|
107
|
+
def anchored_digest_regexp
|
108
|
+
::Regexp.new(anchored_digest)
|
109
|
+
end
|
110
|
+
|
111
|
+
def name_pat
|
112
|
+
expression(optional(domain, literal("/")), name_component, repeated(literal("/"), name_component))
|
113
|
+
end
|
114
|
+
|
115
|
+
# NameRegexp is the format for the name component of references. The
|
116
|
+
# regexp has capturing groups for the domain and name part omitting
|
117
|
+
# the separating forward slash from either.
|
118
|
+
def name_regexp
|
119
|
+
::Regexp.new(name_pat)
|
120
|
+
end
|
121
|
+
|
122
|
+
def anchored_name
|
123
|
+
anchored(optional(capture(domain), literal("/")), capture(name_component, repeated(literal("/"), name_component)))
|
124
|
+
end
|
125
|
+
|
126
|
+
# anchoredNameRegexp is used to parse a name value, capturing the domain and trailing components.
|
127
|
+
def anchored_name_regexp
|
128
|
+
::Regexp.new(anchored_name)
|
129
|
+
end
|
130
|
+
|
131
|
+
def reference_pat
|
132
|
+
anchored(capture(name_pat), optional(literal(":"), capture(tag)), optional(literal("@"), capture(digest_pat)))
|
133
|
+
end
|
134
|
+
|
135
|
+
# ReferenceRegexp is the full supported format of a reference. The regexp
|
136
|
+
# is anchored and has capturing groups for name, tag, and digest components.
|
137
|
+
def reference_regexp
|
138
|
+
::Regexp.new(reference_pat)
|
139
|
+
end
|
140
|
+
|
141
|
+
def identifier
|
142
|
+
"([a-f0-9]{64})"
|
143
|
+
end
|
144
|
+
|
145
|
+
# IdentifierRegexp is the format for string identifier used as a content addressable identifier using sha256.
|
146
|
+
# These identifiers are like digests without the algorithm, since sha256 is used.
|
147
|
+
def identifier_regexp
|
148
|
+
::Regexp.new(identifier)
|
149
|
+
end
|
150
|
+
|
151
|
+
def short_identifier
|
152
|
+
"([a-f0-9]{6,64})"
|
153
|
+
end
|
154
|
+
|
155
|
+
# ShortIdentifierRegexp is the format used to represent a prefix of an identifier.
|
156
|
+
# A prefix may be used to match a sha256 identifier within a list of trusted identifiers.
|
157
|
+
def short_identifier_regexp
|
158
|
+
::Regexp.new(short_identifier)
|
159
|
+
end
|
160
|
+
|
161
|
+
def anchored_identifier
|
162
|
+
anchored(identifier)
|
163
|
+
end
|
164
|
+
|
165
|
+
# anchoredIdentifierRegexp is used to check or match an # identifier value, anchored at start and end of string.
|
166
|
+
def anchored_identifier_regexp
|
167
|
+
::Regexp.new(anchored_identifier)
|
168
|
+
end
|
169
|
+
|
170
|
+
def anchored_short_identifier
|
171
|
+
anchored(short_identifier)
|
172
|
+
end
|
173
|
+
|
174
|
+
# anchoredShortIdentifierRegexp is used to check if a value is a possible identifier prefix, anchored at start and end of string.
|
175
|
+
def anchored_short_identifier_regexp
|
176
|
+
::Regexp.new(anchored_short_identifier)
|
177
|
+
end
|
178
|
+
|
179
|
+
def anchored_encoded_regexp(type)
|
180
|
+
map = {
|
181
|
+
SHA256: ::Regexp.new("^[a-f0-9]{64}$"),
|
182
|
+
SHA384: ::Regexp.new("^[a-f0-9]{96}$"),
|
183
|
+
SHA512: ::Regexp.new("^[a-f0-9]{128}$")
|
184
|
+
}
|
185
|
+
map[type.to_sym]
|
186
|
+
end
|
187
|
+
|
188
|
+
# literal compiles s into a literal regular expression, escaping any regexp reserved characters.
|
189
|
+
def literal(str)
|
190
|
+
re = ::Regexp.new(::Regexp.quote(str))
|
191
|
+
re.source
|
192
|
+
end
|
193
|
+
|
194
|
+
# expression defines a full expression, where each regular expression must follow the previous.
|
195
|
+
def expression(*res)
|
196
|
+
res.join("")
|
197
|
+
end
|
198
|
+
|
199
|
+
# group wraps the regexp in a non-capturing group.
|
200
|
+
def group(*res)
|
201
|
+
"(?:#{expression(*res)})"
|
202
|
+
end
|
203
|
+
|
204
|
+
# optional wraps the expression in a non-capturing group and makes the production optional.
|
205
|
+
def optional(*res)
|
206
|
+
"#{group(expression(*res))}?"
|
207
|
+
end
|
208
|
+
|
209
|
+
# anchored anchors the regular expression by adding start and end delimiters.
|
210
|
+
def anchored(*res)
|
211
|
+
"^#{expression(*res)}$"
|
212
|
+
end
|
213
|
+
|
214
|
+
# repeated wraps the regexp in a non-capturing group to get zero or more matches.
|
215
|
+
def repeated(*res)
|
216
|
+
group("#{group(expression(*res))}*")
|
217
|
+
end
|
218
|
+
|
219
|
+
# capture wraps the expression in a capturing group.
|
220
|
+
def capture(*res)
|
221
|
+
"(#{expression(*res)})"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class Repository
|
5
|
+
attr_accessor :domain, :path
|
6
|
+
|
7
|
+
def initialize(domain = nil, path = nil)
|
8
|
+
@domain = domain
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
return path if Helpers.empty?(domain)
|
14
|
+
|
15
|
+
[domain, path].join("/")
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
name
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
repository: name,
|
25
|
+
domain: domain,
|
26
|
+
path: path
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def familiar
|
31
|
+
Normalize.familiarize_name(self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class TaggedReference
|
5
|
+
extend ::Forwardable
|
6
|
+
def_delegators :repository, :name, :domain, :path
|
7
|
+
|
8
|
+
attr_accessor :repository, :tag
|
9
|
+
|
10
|
+
def initialize(repository = nil, tag = nil)
|
11
|
+
@repository = repository
|
12
|
+
@tag = tag
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
[@repository.name, tag].join(":")
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
{
|
21
|
+
repository: name,
|
22
|
+
domain: domain,
|
23
|
+
path: path,
|
24
|
+
tag: tag
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def familiar
|
29
|
+
self.class.new(Normalize.familiarize_name(self), tag)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Grammar
|
4
|
+
#
|
5
|
+
# reference := name [ ":" tag ] [ "@" digest ]
|
6
|
+
# name := [domain '/'] path-component ['/' path-component]*
|
7
|
+
# domain := host [':' port-number]
|
8
|
+
# host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A
|
9
|
+
# domain-name := domain-component ['.' domain-component]*
|
10
|
+
# domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
|
11
|
+
# port-number := /[0-9]+/
|
12
|
+
# path-component := alpha-numeric [separator alpha-numeric]*
|
13
|
+
# alpha-numeric := /[a-z0-9]+/
|
14
|
+
# separator := /[_.]|__|[-]*/
|
15
|
+
#
|
16
|
+
# tag := /[\w][\w.-]{0,127}/
|
17
|
+
#
|
18
|
+
# digest := digest-algorithm ":" digest-hex
|
19
|
+
# digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
|
20
|
+
# digest-algorithm-separator := /[+.-_]/
|
21
|
+
# digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
|
22
|
+
# digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
|
23
|
+
#
|
24
|
+
# identifier := /[a-f0-9]{64}/
|
25
|
+
# short-identifier := /[a-f0-9]{6,64}/
|
26
|
+
|
27
|
+
require "forwardable"
|
28
|
+
|
29
|
+
require_relative "docker_distribution/version"
|
30
|
+
require_relative "docker_distribution/errors"
|
31
|
+
require_relative "docker_distribution/helpers"
|
32
|
+
|
33
|
+
require_relative "docker_distribution/digest"
|
34
|
+
require_relative "docker_distribution/digest_set"
|
35
|
+
|
36
|
+
require_relative "docker_distribution/regexp"
|
37
|
+
require_relative "docker_distribution/repository"
|
38
|
+
require_relative "docker_distribution/reference"
|
39
|
+
require_relative "docker_distribution/tagged_reference"
|
40
|
+
require_relative "docker_distribution/canonical_reference"
|
41
|
+
require_relative "docker_distribution/digest_reference"
|
42
|
+
require_relative "docker_distribution/normalize"
|
43
|
+
|
44
|
+
# Package reference provides a general type to represent any way of referencing images within the registry.
|
45
|
+
# Its main purpose is to abstract tags and digests (content-addressable hash).
|
46
|
+
module DockerDistribution
|
47
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: docker_distribution
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Artem Petrov
|
@@ -44,7 +44,22 @@ email:
|
|
44
44
|
executables: []
|
45
45
|
extensions: []
|
46
46
|
extra_rdoc_files: []
|
47
|
-
files:
|
47
|
+
files:
|
48
|
+
- README.md
|
49
|
+
- Rakefile
|
50
|
+
- lib/docker_distribution.rb
|
51
|
+
- lib/docker_distribution/canonical_reference.rb
|
52
|
+
- lib/docker_distribution/digest.rb
|
53
|
+
- lib/docker_distribution/digest_reference.rb
|
54
|
+
- lib/docker_distribution/digest_set.rb
|
55
|
+
- lib/docker_distribution/errors.rb
|
56
|
+
- lib/docker_distribution/helpers.rb
|
57
|
+
- lib/docker_distribution/normalize.rb
|
58
|
+
- lib/docker_distribution/reference.rb
|
59
|
+
- lib/docker_distribution/regexp.rb
|
60
|
+
- lib/docker_distribution/repository.rb
|
61
|
+
- lib/docker_distribution/tagged_reference.rb
|
62
|
+
- lib/docker_distribution/version.rb
|
48
63
|
homepage: https://github.com/artempartos/docker_distribution
|
49
64
|
licenses:
|
50
65
|
- MIT
|