docker_distribution 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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, optional(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, optional(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, optional(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, optional(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 one or more matches.
215
+ def repeated(*res)
216
+ "#{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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerDistribution
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,4 @@
1
+ module DockerDistribution
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end