oci_registry 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 929ac794aafd238965aad937e7f2a1cb197986e582893d5e4051c93e0098a88a
4
+ data.tar.gz: 64e51f1abdbd0176f6d337a72b4c1bd1e261f3c9f599aaddd78311caadcd3070
5
+ SHA512:
6
+ metadata.gz: b41dbb62fb262b588b43d5503c782a3d43857dc4b79dceed24e428666910f88bdaaabde7c6fccc957cfa9b661f1aad11d07e5dce5f3bd73c870846ed382299e3
7
+ data.tar.gz: a5f268cfae9dd9372f7ff8a62c9c5cef9514969e2d96ab14c1f203e28cb0df6e5c38d2486a57dcd9e50dcb51e56716b2b107efe2b4767d633d93323a9b9df200
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2024-07-30
4
+
5
+ ### Added
6
+ - Initial release
7
+ - OCI/Docker registry client for interacting with registries
8
+ - Support for authentication (username/password and token)
9
+ - URI parser for Docker/OCI URIs
10
+ - Utility functions for working with images and layers
11
+ - Support for manifest operations
12
+ - Support for tag listing
13
+ - Support for blob operations
14
+ - Support for image copying between registries
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OCIRegistry Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # OCI Registry
2
+
3
+ A Ruby library for interacting with OCI/Docker registries using the native HTTP/tar/sha format to navigate repositories and retrieve tags, metadata and other useful information.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'oci_registry'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ ## Usage
18
+
19
+ ### Client
20
+
21
+ ```ruby
22
+ # Create a client for Docker Hub
23
+ client = OCIRegistry::Client.new(username: 'user', password: 'pass')
24
+
25
+ # Create a client for Google Container Registry
26
+ client = OCIRegistry::Client.new(token: 'access-token', host: 'gcr.io')
27
+
28
+ # Get image metadata
29
+ metadata = client.metadata('library/nginx', 'latest')
30
+
31
+ # List tags
32
+ client.tags('library/nginx') do |tag|
33
+ puts tag
34
+ end
35
+
36
+ # Get manifest
37
+ manifest = client.manifest('library/nginx', 'latest')
38
+
39
+ # Download and extract layers
40
+ layers = manifest['layers']
41
+ client.download_and_extract_layers('library/nginx', layers, 'config.json')
42
+ ```
43
+
44
+ ### URI Parser
45
+
46
+ ```ruby
47
+ # Parse Docker/OCI URIs
48
+ uri = OCIRegistry::URI.new('docker://docker.io/library/nginx:latest')
49
+ puts uri.repository # => "library/nginx"
50
+ puts uri.tag # => "latest"
51
+ puts uri.host # => "docker.io"
52
+ ```
53
+
54
+ ### Utils
55
+
56
+ ```ruby
57
+ # Write skopeo policy file
58
+ OCIRegistry::Utils.write_policy(filename: "policy.json")
59
+
60
+ # Calculate SHA256 of a file
61
+ sha = OCIRegistry::Utils.calculate_sha256("/path/to/file")
62
+
63
+ # Sum layers from skopeo inspect output
64
+ size = OCIRegistry::Utils.sum_layers(skopeo_inspect_json)
65
+
66
+ # Copy image between registries
67
+ digest = OCIRegistry::Utils.copy_image(
68
+ src_user: "user1", src_pass: "pass1",
69
+ dst_user: "user2", dst_pass: "pass2",
70
+ src_oci: "docker.io/library/nginx",
71
+ src_commit: "latest",
72
+ dst_oci: "my-registry.com/nginx",
73
+ dst_commit: "latest"
74
+ )
75
+ ```
76
+
77
+ ## Development
78
+
79
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
80
+
81
+ ## Contributing
82
+
83
+ Bug reports and pull requests are welcome.
84
+
85
+ ## License
86
+
87
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,57 @@
1
+ module OCIRegistry
2
+ class Client
3
+ attr_accessor :username, :password, :token, :host
4
+ def initialize(username: nil, password: nil, token: nil, host: 'registry-1.docker.io')
5
+ # Main Execution Flow
6
+ @username = username
7
+ @password = password
8
+ @token = token # e.g. gcloud auth print-access-token
9
+ @tokens = {}
10
+ @manifests = {}
11
+ @host = host
12
+ end
13
+ def metadata(repository, tag)
14
+ manifest = self.manifest(repository, tag)
15
+ case manifest['mediaType']
16
+ when "application/vnd.docker.distribution.manifest.list.v2+json"
17
+ # Multi-arch images have a manifest list
18
+ manifest = manifest['manifests'].find { |m| m['platform']['architecture'] == 'amd64' && m['platform']['os'] == 'linux' }
19
+ self.metadata(repository, manifest['digest'])
20
+ when "application/vnd.docker.distribution.manifest.v2+json"
21
+ # Builders
22
+ blob_digest = manifest['config']['digest']
23
+ OCIRegistry::Remote.get_blob(host, self.token(repository), repository, blob_digest)
24
+ when "application/vnd.oci.image.index.v1+json"
25
+ # Buildpacks give a collection of manifests as an index if they are multi-arch:
26
+ manifest = manifest['manifests'].find { |s| s['platform']['architecture'] == 'amd64' && s['platform']['os'] == 'linux' }
27
+ self.metadata(repository, manifest['digest'])
28
+ when "application/vnd.oci.image.manifest.v1+json"
29
+ # OCI Images
30
+ blob_digest = manifest['config']['digest']
31
+ OCIRegistry::Remote.get_blob(host, self.token(repository), repository, blob_digest)
32
+ else
33
+ raise "Unknown manifest type: #{manifest['mediaType']}"
34
+ end
35
+ end
36
+ def token(repository)
37
+ return @token if @token # e.g. Google Container Registry
38
+ @tokens[repository] = OCIRegistry::Remote.get_docker_token(repository, @username, @password)
39
+ end
40
+ def manifest(repository, tag)
41
+ slug = "#{repository}:#{tag}"
42
+ @manifests[slug] = OCIRegistry::Remote.get_manifest(self.host, self.token(repository), repository, tag)
43
+ end
44
+ def tags(repository, &block)
45
+ tags = OCIRegistry::Remote.get_tags(self.host, self.token(repository), repository)
46
+ return tags.each(&block) if block_given?
47
+ tags
48
+ end
49
+ def download_and_extract_layers(repository, layers, file_name)
50
+ OCIRegistry::Remote.download_and_extract_layers(host, self.token(repository), repository, layers, file_name)
51
+ end
52
+ def ping
53
+ # ping the api
54
+ OCIRegistry::Remote.http_get(URI("https://#{host}/v2/"), { 'Authorization' => "Bearer #{@token}" })
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,193 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'json'
4
+ require 'optparse'
5
+ require 'base64'
6
+ require 'zlib'
7
+ require 'minitar'
8
+ require 'stringio'
9
+ require 'fileutils'
10
+
11
+
12
+ module OCIRegistry
13
+ class Remote
14
+
15
+ # Helper method to handle HTTP GET requests with redirect support
16
+ def self.http_get(uri, headers = {}, limit = 10)
17
+ raise 'Too many HTTP redirects' if limit == 0
18
+
19
+ request = Net::HTTP::Get.new(uri)
20
+ headers.each { |k, v| request[k] = v }
21
+
22
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
23
+ http.request(request)
24
+ end
25
+
26
+ case response
27
+ when Net::HTTPRedirection
28
+ location = response['location']
29
+ # warn "Redirected to #{location}"
30
+ # Handle the case where location is a relative URL by reconstructing the full URL
31
+ new_uri = ::URI.join(uri, location)
32
+ http_get(new_uri, headers, limit - 1)
33
+ # 429
34
+ when Net::HTTPTooManyRequests
35
+ puts "Rate limited: #{response.code} #{response.message}"
36
+ # Retry after the specified time
37
+ retry_after = response['Retry-After'].to_i || 10
38
+ sleep(retry_after)
39
+ http_get(uri, headers, limit - 1)
40
+ else
41
+ response
42
+ end
43
+ end
44
+
45
+ # Step 1: Obtain an Access Token
46
+ def self.get_docker_token(repository, username = nil, password = nil)
47
+ uri = URI("https://auth.docker.io/token?service=registry.docker.io&scope=repository:#{repository}:pull")
48
+
49
+ headers = {}
50
+ if username && password
51
+ encoded_credentials = Base64.strict_encode64("#{username}:#{password}")
52
+ headers['Authorization'] = "Basic #{encoded_credentials}"
53
+ end
54
+
55
+ response = http_get(uri, headers)
56
+
57
+ if response.is_a?(Net::HTTPSuccess)
58
+ json = JSON.parse(response.body)
59
+ json['token']
60
+ else
61
+ raise "Failed to obtain token: #{response.code} #{response.message}"
62
+ end
63
+ end
64
+
65
+ # Step 2: Fetch the Image Manifest
66
+ def self.get_manifest(host, token, repository, tag)
67
+ uri = URI("https://#{host}/v2/#{repository}/manifests/#{tag}")
68
+ headers = {
69
+ 'Authorization' => "Bearer #{token}",
70
+ 'Accept' => [
71
+ 'application/vnd.oci.image.index.v1+json',
72
+ 'application/vnd.docker.distribution.manifest.list.v2+json',
73
+ 'application/vnd.oci.image.manifest.v1+json',
74
+ 'application/vnd.docker.distribution.manifest.v2+json'
75
+ ].join(', ')
76
+ }
77
+
78
+ response = http_get(uri, headers)
79
+
80
+ if response.is_a?(Net::HTTPSuccess)
81
+ JSON.parse(response.body)
82
+ else
83
+ raise "Failed to fetch manifest: #{response.code} #{response.message}"
84
+ end
85
+ end
86
+
87
+ # Step 4: Retrieve the Image Configuration
88
+ def self.get_blob(host, token, repository, blob_digest)
89
+ uri = URI("https://#{host}/v2/#{repository}/blobs/#{blob_digest}")
90
+ headers = { 'Authorization' => "Bearer #{token}" }
91
+
92
+ response = http_get(uri, headers)
93
+
94
+ if response.is_a?(Net::HTTPSuccess)
95
+ JSON.parse(response.body)
96
+ else
97
+ raise "Failed to retrieve image configuration for #{uri.inspect}: #{response.code} #{response.message}"
98
+ end
99
+ end
100
+
101
+ # Function to fetch the list of tags for the repository
102
+ def self.get_tags(host, token, repository)
103
+ tags = []
104
+ batch_size = 100
105
+ next_url = "/v2/#{repository}/tags/list?n=#{batch_size}"
106
+ headers = {
107
+ 'Authorization' => "Bearer #{token}",
108
+ 'Accept' => 'application/json'
109
+ }
110
+
111
+ loop do
112
+ uri = URI("https://#{host}#{next_url}")
113
+ response = http_get(uri, headers)
114
+
115
+ if response.is_a?(Net::HTTPSuccess)
116
+ json = JSON.parse(response.body)
117
+ tags.concat(json['tags'] || [])
118
+
119
+ # Check for pagination
120
+ link_header = response['Link']
121
+ if link_header && link_header.include?('rel="next"')
122
+ next_url = link_header.match(/<(.+)>;/)[1]
123
+ else
124
+ break
125
+ end
126
+ else
127
+ puts "Failed to fetch tags for #{repository}: https://#{host}#{next_url} #{response.code} #{response.message}"
128
+ break
129
+ end
130
+ end
131
+
132
+ tags
133
+ end
134
+
135
+ # Step 5: Extract Labels and Metadata
136
+ def self.display_metadata(config_json)
137
+ labels = config_json['config']['Labels']
138
+ puts "Labels:"
139
+ puts JSON.pretty_generate(labels)
140
+
141
+ puts "\nOther Metadata:"
142
+ metadata = config_json['config']
143
+ puts JSON.pretty_generate(metadata)
144
+ end
145
+
146
+ # Function to download and extract layers
147
+ def self.download_and_extract_layers(host, token, repository, layers, file_name)
148
+ layers.each_with_index do |layer, index|
149
+ digest = layer['digest']
150
+ puts "Processing layer #{index + 1}/#{layers.size}: #{digest}"
151
+
152
+ # Download the layer blob
153
+ uri = URI("https://#{host}/v2/#{repository}/blobs/#{digest}")
154
+ headers = { 'Authorization' => "Bearer #{token}" }
155
+
156
+ response = http_get(uri, headers)
157
+
158
+ if response.is_a?(Net::HTTPSuccess)
159
+ # Decompress and extract the tar.gz layer
160
+ found = extract_layer(response.body, file_name)
161
+ return true if found
162
+ else
163
+ puts "Failed to download layer #{digest}: #{response.code} #{response.message}"
164
+ end
165
+ end
166
+ false
167
+ end
168
+
169
+ # Function to extract a layer and search for the specified file
170
+ def self.extract_layer(layer_data, file_name)
171
+ # Decompress the gzip layer data
172
+ gz = Zlib::GzipReader.new(StringIO.new(layer_data))
173
+ tar_io = StringIO.new(gz.read)
174
+
175
+ # Extract tar contents
176
+ Minitar::Input.open(tar_io) do |tar|
177
+ tar.each do |entry|
178
+ # Normalize file paths to handle different directory structures
179
+ entry_path = entry.full_name.sub(/^\.\//, '').sub(/^\/+/, '')
180
+
181
+ if File.basename(entry_path) == file_name
182
+ puts "\nFound '#{file_name}' in layer:"
183
+ contents = entry.read
184
+ puts contents
185
+ return true
186
+ end
187
+ end
188
+ end
189
+ false
190
+ end
191
+
192
+ end
193
+ end
@@ -0,0 +1,58 @@
1
+ require 'uri'
2
+
3
+ # Define a custom class for Docker URIs
4
+ module OCIRegistry
5
+ class URI < URI::Generic
6
+ attr_accessor :repository, :tag, :digest, :host, :scheme
7
+
8
+ def initialize(uri)
9
+ raise ArgumentError, 'OCIRegistry::URI->URI cannot be nil' if uri.nil?
10
+ raise ArgumentError, 'OCIRegistry::URI->URI must be a String' unless uri.is_a?(String)
11
+
12
+ # TODO: This addition makes this less an OCI URI library and more a CNB Builder Resource Library
13
+ # Handle the case where the URI is just ./eol-buildpack/ which is a local path.
14
+ @repository = uri and @scheme = 'local' and return if uri.start_with?('./')
15
+
16
+ # Call the parent class constructor
17
+ @parsed = ::URI.parse(uri) # Use `::URI` to refer to Ruby's URI class
18
+
19
+ # Ensure the scheme is docker
20
+ unless ['docker', 'oci'].include?(@parsed.scheme)
21
+ raise ArgumentError, 'URI must start with (docker|oci)://'
22
+ end
23
+
24
+ # Extract repository, tag, and digest from the URI's path
25
+ @scheme = @parsed.scheme
26
+ @host = @parsed.host
27
+ @repository, tag_or_digest = @parsed.path.split('@').first.split(':')
28
+ # Remove any leading slashes from the repository
29
+ @repository = @repository.sub(%r{^/*}, '')
30
+ @digest = @parsed.path.split('@')[1]
31
+ @tag = tag_or_digest unless @digest
32
+ end
33
+
34
+ # Allow for
35
+ # heroku/ruby
36
+ # heroku/ruby:2.7.2
37
+ # heroku/ruby@sha256:1234abcd
38
+ # docker://docker.io/heroku/ruby
39
+ # docker://docker.io/heroku/ruby:2.7.2
40
+ def to_s
41
+ base = ""
42
+ base += "#{@scheme}://" if @scheme
43
+ base += "#{@host}/" if @host
44
+ base += "#{@repository}" if @repository
45
+ base += ":#{@tag}" if @tag
46
+ base += "@#{@digest}" if @digest
47
+ base
48
+ end
49
+ end
50
+ end
51
+
52
+ # Examples of parsing
53
+ # uri1 = DockerURI.new('docker://docker.io/heroku/buildpack-dot@sha256:123')
54
+ # uri2 = DockerURI.new('docker://docker.io/heroku/buildpack-dot:3.1.2')
55
+
56
+ # puts "Parsed URI1: Repository=#{uri1.repository}, Tag=#{uri1.tag}, Digest=#{uri1.digest}"
57
+ # puts "Parsed URI2: Repository=#{uri2.repository}, Tag=#{uri2.tag}, Digest=#{uri2.digest}"
58
+
@@ -0,0 +1,119 @@
1
+ # OCI related functions. Generally using skopeo and umoci. These span U9s (Heroku slug repackaging) and
2
+ # Qack (CNB "pack" rebuilding).
3
+ # Resources:
4
+ # - [Manual OCI building](https://ravichaganti.com/blog/2022-11-28-building-container-images-using-no-tools/)
5
+ require 'shellwords'
6
+ require 'open3'
7
+ require 'json'
8
+ require 'digest'
9
+ require 'tmpdir'
10
+
11
+ module OCIRegistry
12
+ class Utils
13
+ # Helper method to run shell commands
14
+ def self.run_command(cmd)
15
+ stdout, stderr, status = Open3.capture3(cmd)
16
+ {
17
+ out: stdout,
18
+ err: stderr,
19
+ status: status.exitstatus
20
+ }
21
+ end
22
+
23
+ def self.get_image_digest(image_name)
24
+ end
25
+
26
+ # Function to calculate SHA256 hash of a file
27
+ def self.calculate_sha256(file_path)
28
+ Digest::SHA256.file(file_path).hexdigest
29
+ end
30
+
31
+ def self.write_policy(filename: "policy.json")
32
+ File.open(filename, "w") do |f|
33
+ f.write <<~EOF
34
+ {
35
+ "default": [
36
+ {
37
+ "type": "insecureAcceptAnything"
38
+ }
39
+ ],
40
+ "transports":
41
+ {
42
+ "docker-daemon":
43
+ {
44
+ "": [{"type":"insecureAcceptAnything"}]
45
+ }
46
+ }
47
+ }
48
+ EOF
49
+ end
50
+ end
51
+
52
+ # Takes the output of skopeo inspect and returns the size of the image by adding its layers.
53
+ # cmd = %Q[skopeo inspect oci:#{td}/tmp_stack:latest]
54
+ # skopeo_inspect = JSON.parse(`#{cmd}`)
55
+ def self.sum_layers(skopeo_inspect)
56
+ # Ensure we treat as JSON...
57
+ skopeo_inspect = JSON.parse(skopeo_inspect) if skopeo_inspect.is_a?(String)
58
+ checksum = skopeo_inspect['Digest']
59
+ if skopeo_inspect['LayersData'].nil?
60
+ puts "skopeo_inspect: #{skopeo_inspect.to_yaml}"
61
+ raise "No layers found."
62
+ end
63
+ size = skopeo_inspect['LayersData'].map { |l| l['Size'] }.sum
64
+ end
65
+
66
+ def self.copy_image(src_user:, src_pass:, dst_user:, dst_pass:, src_oci:, src_commit:, dst_oci:, dst_commit:)
67
+ src_user = Shellwords.escape(src_user)
68
+ src_pass = Shellwords.escape(src_pass)
69
+ dst_user = Shellwords.escape(dst_user)
70
+ dst_pass = Shellwords.escape(dst_pass)
71
+
72
+ copy(src_user: src_user, src_pass: src_pass, dst_user: dst_user, dst_pass: dst_pass, src_oci: src_oci, src_commit: src_commit, dst_oci: dst_oci, dst_commit: dst_commit)
73
+ cmd = %Q[skopeo inspect --creds #{dst_user}:#{dst_pass} docker://#{dst_oci}:#{dst_commit}]
74
+ details = run_command(cmd)
75
+ if details[:status] == 1
76
+ # Error occured
77
+ # Logger: ("Error inspecting image:\n#{details[:err]}")
78
+ raise "Image not found in registry #{details[:err]}."
79
+ end
80
+ inspect = JSON.parse(details[:out])
81
+ inspect['Digest']
82
+ end
83
+
84
+ # TODO: Copy FROM registry->local then local->registry. This avoids an error where a slow vs fast registry
85
+ # [causes a timeout](https://github.com/containers/image/issues/1083).
86
+ def self.copy(src_user:, src_pass:, dst_user:, dst_pass:, src_oci:, src_commit:, dst_oci:, dst_commit:)
87
+ retries = 0
88
+ begin
89
+ details = {}
90
+ # We need a tempdir to write the policy file
91
+ Dir.mktmpdir do |dir|
92
+ OCIRegistry::Utils.write_policy(filename: File.join(dir, "policy.json"))
93
+ cmd = %Q[skopeo --policy #{dir}/policy.json copy --src-creds #{src_user}:#{src_pass} --dest-creds #{dst_user}:#{dst_pass} docker://#{src_oci}:#{src_commit} docker://#{dst_oci}:#{dst_commit}]
94
+ details = run_command(cmd)
95
+ # Dir.mktmpdir do |tar|
96
+ # cmd = %Q[skopeo --policy #{dir}/policy.json copy --src-creds #{src_user}:#{src_pass} docker://#{src_oci}:#{src_commit} dir:#{tar}]
97
+ # details = run_command(cmd)
98
+ # cmd = %Q[skopeo --policy #{dir}/policy.json copy --dest-creds #{dst_user}:#{dst_pass} dir:#{tar} docker://#{dst_oci}:#{dst_commit}]
99
+ # details = run_command(cmd)
100
+ # end
101
+ end
102
+ if details[:status] == 1
103
+ # Error occured
104
+ # Logger: ("Error copying image:\n#{details[:err]}")
105
+ raise "Error copying image from registry to registry #{details[:err]}."
106
+ end
107
+ details
108
+ rescue Exception => e
109
+ # Logger: ("Error caught copying image: #{e.message}.")
110
+ if (retries += 1) < 3
111
+ # Logger: ("Retry... ##{retries}.")
112
+ sleep 10 * 2**retries # Exponential backoff
113
+ retry
114
+ end
115
+ raise e
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OCIRegistry
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "oci_registry/version"
4
+ require "oci_registry/client"
5
+ require "oci_registry/remote"
6
+ require "oci_registry/uri"
7
+ require "oci_registry/utils"
8
+
9
+ module OCIRegistry
10
+ class Error < StandardError; end
11
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: oci_registry
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Siegel
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-07-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: shellwords
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitar
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webmock
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.18'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.18'
83
+ - !ruby/object:Gem::Dependency
84
+ name: vcr
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '6.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '6.1'
97
+ description: A Ruby library for interacting with OCI/Docker registries using the native
98
+ HTTP/tar/sha format to navigate repositories and retrieve tags, metadata and other
99
+ useful information.
100
+ email:
101
+ - usiegj00@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - CHANGELOG.md
107
+ - LICENSE.txt
108
+ - README.md
109
+ - lib/oci_registry.rb
110
+ - lib/oci_registry/client.rb
111
+ - lib/oci_registry/remote.rb
112
+ - lib/oci_registry/uri.rb
113
+ - lib/oci_registry/utils.rb
114
+ - lib/oci_registry/version.rb
115
+ homepage: https://github.com/usiegj00/oci_registry
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: 2.7.0
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.5.22
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: Ruby client for OCI/Docker Registry HTTP API v2
138
+ test_files: []