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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE.txt +21 -0
- data/README.md +87 -0
- data/lib/oci_registry/client.rb +57 -0
- data/lib/oci_registry/remote.rb +193 -0
- data/lib/oci_registry/uri.rb +58 -0
- data/lib/oci_registry/utils.rb +119 -0
- data/lib/oci_registry/version.rb +5 -0
- data/lib/oci_registry.rb +11 -0
- metadata +138 -0
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
|
data/lib/oci_registry.rb
ADDED
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: []
|