docker_distribution 0.1.0

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.
@@ -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