occystrap 0.4.0__py3-none-any.whl → 0.4.1__py3-none-any.whl
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.
- occystrap/_version.py +34 -0
- occystrap/filters/__init__.py +10 -0
- occystrap/filters/base.py +67 -0
- occystrap/filters/exclude.py +136 -0
- occystrap/filters/inspect.py +179 -0
- occystrap/filters/normalize_timestamps.py +123 -0
- occystrap/filters/search.py +177 -0
- occystrap/inputs/__init__.py +1 -0
- occystrap/inputs/base.py +40 -0
- occystrap/inputs/docker.py +171 -0
- occystrap/{docker_registry.py → inputs/registry.py} +112 -50
- occystrap/inputs/tarfile.py +88 -0
- occystrap/main.py +330 -31
- occystrap/outputs/__init__.py +1 -0
- occystrap/outputs/base.py +46 -0
- occystrap/{output_directory.py → outputs/directory.py} +10 -9
- occystrap/outputs/docker.py +137 -0
- occystrap/{output_mounts.py → outputs/mounts.py} +2 -1
- occystrap/{output_ocibundle.py → outputs/ocibundle.py} +1 -1
- occystrap/outputs/registry.py +240 -0
- occystrap/{output_tarfile.py → outputs/tarfile.py} +18 -2
- occystrap/pipeline.py +297 -0
- occystrap/tarformat.py +122 -0
- occystrap/tests/test_inspect.py +355 -0
- occystrap/tests/test_tarformat.py +199 -0
- occystrap/uri.py +231 -0
- occystrap/util.py +67 -38
- occystrap-0.4.1.dist-info/METADATA +444 -0
- occystrap-0.4.1.dist-info/RECORD +38 -0
- {occystrap-0.4.0.dist-info → occystrap-0.4.1.dist-info}/WHEEL +1 -1
- {occystrap-0.4.0.dist-info → occystrap-0.4.1.dist-info}/entry_points.txt +0 -1
- occystrap/docker_extract.py +0 -36
- occystrap-0.4.0.dist-info/METADATA +0 -131
- occystrap-0.4.0.dist-info/RECORD +0 -20
- occystrap-0.4.0.dist-info/pbr.json +0 -1
- {occystrap-0.4.0.dist-info → occystrap-0.4.1.dist-info/licenses}/AUTHORS +0 -0
- {occystrap-0.4.0.dist-info → occystrap-0.4.1.dist-info/licenses}/LICENSE +0 -0
- {occystrap-0.4.0.dist-info → occystrap-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Load images into the local Docker or Podman daemon via the Docker Engine API.
|
|
2
|
+
# This communicates over a Unix domain socket (default: /var/run/docker.sock).
|
|
3
|
+
#
|
|
4
|
+
# Docker Engine API documentation:
|
|
5
|
+
# https://docs.docker.com/engine/api/
|
|
6
|
+
#
|
|
7
|
+
# Podman compatibility:
|
|
8
|
+
# Podman provides a Docker-compatible API via podman.socket. Use the socket
|
|
9
|
+
# option to point to the Podman socket:
|
|
10
|
+
# - Rootful: /run/podman/podman.sock
|
|
11
|
+
# - Rootless: /run/user/<uid>/podman/podman.sock
|
|
12
|
+
# See: https://docs.podman.io/en/latest/markdown/podman-system-service.1.html
|
|
13
|
+
#
|
|
14
|
+
# The API accepts images in the same format as 'docker load', which is the
|
|
15
|
+
# v1.2 tarball format that outputs/tarfile.py creates.
|
|
16
|
+
|
|
17
|
+
import io
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
import tarfile
|
|
22
|
+
import tempfile
|
|
23
|
+
|
|
24
|
+
import requests_unixsocket
|
|
25
|
+
|
|
26
|
+
from occystrap import constants
|
|
27
|
+
from occystrap.outputs.base import ImageOutput
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LOG = logging.getLogger(__name__)
|
|
31
|
+
LOG.setLevel(logging.INFO)
|
|
32
|
+
|
|
33
|
+
DEFAULT_SOCKET_PATH = '/var/run/docker.sock'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DockerWriter(ImageOutput):
|
|
37
|
+
"""Loads images into the local Docker daemon.
|
|
38
|
+
|
|
39
|
+
This output writer builds a v1.2 format tarball and loads it into
|
|
40
|
+
the Docker daemon using the POST /images/load API endpoint. This is
|
|
41
|
+
equivalent to running 'docker load'.
|
|
42
|
+
|
|
43
|
+
Uses USTAR format for the outer tarball which contains only short paths
|
|
44
|
+
(SHA256 hashes and small filenames), avoiding PAX extended headers.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, image, tag, socket_path=DEFAULT_SOCKET_PATH):
|
|
48
|
+
"""Initialize the Docker writer.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
image: The image name.
|
|
52
|
+
tag: The image tag.
|
|
53
|
+
socket_path: Path to the Docker socket (default: /var/run/docker.sock).
|
|
54
|
+
"""
|
|
55
|
+
self.image = image
|
|
56
|
+
self.tag = tag
|
|
57
|
+
self.socket_path = socket_path
|
|
58
|
+
self._session = None
|
|
59
|
+
|
|
60
|
+
self._temp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
61
|
+
self._image_tar = tarfile.open(fileobj=self._temp_file, mode='w',
|
|
62
|
+
format=tarfile.USTAR_FORMAT)
|
|
63
|
+
|
|
64
|
+
self._tar_manifest = [{
|
|
65
|
+
'Layers': [],
|
|
66
|
+
'RepoTags': ['%s:%s' % (self.image.split('/')[-1], self.tag)]
|
|
67
|
+
}]
|
|
68
|
+
|
|
69
|
+
def _get_session(self):
|
|
70
|
+
if self._session is None:
|
|
71
|
+
self._session = requests_unixsocket.Session()
|
|
72
|
+
return self._session
|
|
73
|
+
|
|
74
|
+
def _socket_url(self, path):
|
|
75
|
+
encoded_socket = self.socket_path.replace('/', '%2F')
|
|
76
|
+
return 'http+unix://%s%s' % (encoded_socket, path)
|
|
77
|
+
|
|
78
|
+
def fetch_callback(self, digest):
|
|
79
|
+
"""Always fetch all layers."""
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def process_image_element(self, element_type, name, data):
|
|
83
|
+
"""Process an image element, adding it to the tarball."""
|
|
84
|
+
if element_type == constants.CONFIG_FILE:
|
|
85
|
+
LOG.info('Adding config file to tarball')
|
|
86
|
+
|
|
87
|
+
ti = tarfile.TarInfo(name)
|
|
88
|
+
ti.size = len(data.read())
|
|
89
|
+
data.seek(0)
|
|
90
|
+
self._image_tar.addfile(ti, data)
|
|
91
|
+
self._tar_manifest[0]['Config'] = name
|
|
92
|
+
|
|
93
|
+
elif element_type == constants.IMAGE_LAYER:
|
|
94
|
+
LOG.info('Adding layer to tarball')
|
|
95
|
+
|
|
96
|
+
name += '/layer.tar'
|
|
97
|
+
ti = tarfile.TarInfo(name)
|
|
98
|
+
data.seek(0, os.SEEK_END)
|
|
99
|
+
ti.size = data.tell()
|
|
100
|
+
data.seek(0)
|
|
101
|
+
self._image_tar.addfile(ti, data)
|
|
102
|
+
self._tar_manifest[0]['Layers'].append(name)
|
|
103
|
+
|
|
104
|
+
def finalize(self):
|
|
105
|
+
"""Write manifest and load the image into Docker."""
|
|
106
|
+
LOG.info('Writing manifest to tarball')
|
|
107
|
+
encoded_manifest = json.dumps(self._tar_manifest).encode('utf-8')
|
|
108
|
+
ti = tarfile.TarInfo('manifest.json')
|
|
109
|
+
ti.size = len(encoded_manifest)
|
|
110
|
+
self._image_tar.addfile(ti, io.BytesIO(encoded_manifest))
|
|
111
|
+
self._image_tar.close()
|
|
112
|
+
|
|
113
|
+
temp_path = self._temp_file.name
|
|
114
|
+
self._temp_file.close()
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
LOG.info('Loading image into Docker daemon at %s' % self.socket_path)
|
|
118
|
+
session = self._get_session()
|
|
119
|
+
url = self._socket_url('/images/load')
|
|
120
|
+
|
|
121
|
+
with open(temp_path, 'rb') as f:
|
|
122
|
+
r = session.post(
|
|
123
|
+
url,
|
|
124
|
+
data=f,
|
|
125
|
+
headers={'Content-Type': 'application/x-tar'}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if r.status_code != 200:
|
|
129
|
+
raise Exception(
|
|
130
|
+
'Docker API error %d: %s' % (r.status_code, r.text))
|
|
131
|
+
|
|
132
|
+
LOG.info('Image loaded successfully: %s:%s'
|
|
133
|
+
% (self.image, self.tag))
|
|
134
|
+
|
|
135
|
+
finally:
|
|
136
|
+
if os.path.exists(temp_path):
|
|
137
|
+
os.unlink(temp_path)
|
|
@@ -7,13 +7,14 @@ import tarfile
|
|
|
7
7
|
from occystrap import common
|
|
8
8
|
from occystrap import constants
|
|
9
9
|
from occystrap import util
|
|
10
|
+
from occystrap.outputs.base import ImageOutput
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
LOG = logging.getLogger(__name__)
|
|
13
14
|
LOG.setLevel(logging.INFO)
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
class MountWriter(
|
|
17
|
+
class MountWriter(ImageOutput):
|
|
17
18
|
def __init__(self, image, tag, image_path):
|
|
18
19
|
self.image = image
|
|
19
20
|
self.tag = tag
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Push images to a Docker/OCI registry via the Docker Registry HTTP API V2.
|
|
2
|
+
#
|
|
3
|
+
# Docker Registry API documentation:
|
|
4
|
+
# https://docs.docker.com/registry/spec/api/
|
|
5
|
+
#
|
|
6
|
+
# OCI Distribution Spec:
|
|
7
|
+
# https://github.com/opencontainers/distribution-spec/blob/main/spec.md
|
|
8
|
+
#
|
|
9
|
+
# The push process:
|
|
10
|
+
# 1. For each layer blob:
|
|
11
|
+
# a. Check if blob exists: HEAD /v2/<name>/blobs/<digest>
|
|
12
|
+
# b. If not, initiate upload: POST /v2/<name>/blobs/uploads/
|
|
13
|
+
# c. Upload blob: PUT <location>?digest=<digest>
|
|
14
|
+
# 2. Upload config blob (same as layer)
|
|
15
|
+
# 3. Push manifest: PUT /v2/<name>/manifests/<tag>
|
|
16
|
+
|
|
17
|
+
import gzip
|
|
18
|
+
import hashlib
|
|
19
|
+
import io
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
|
|
24
|
+
import requests
|
|
25
|
+
|
|
26
|
+
from occystrap import constants
|
|
27
|
+
from occystrap.outputs.base import ImageOutput
|
|
28
|
+
from occystrap import util
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
LOG = logging.getLogger(__name__)
|
|
32
|
+
LOG.setLevel(logging.INFO)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class RegistryWriter(ImageOutput):
|
|
36
|
+
"""Pushes images to a Docker/OCI registry.
|
|
37
|
+
|
|
38
|
+
This output writer uploads image layers and config to a registry
|
|
39
|
+
using the Docker Registry HTTP API V2, then pushes a manifest to
|
|
40
|
+
make the image available.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, registry, image, tag, secure=True,
|
|
44
|
+
username=None, password=None):
|
|
45
|
+
"""Initialize the registry writer.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
registry: Registry hostname (e.g., 'docker.io', 'ghcr.io').
|
|
49
|
+
image: Image name/path (e.g., 'library/busybox', 'myuser/myimage').
|
|
50
|
+
tag: Image tag (e.g., 'latest', 'v1.0').
|
|
51
|
+
secure: If True, use HTTPS (default). If False, use HTTP.
|
|
52
|
+
username: Username for authentication (optional).
|
|
53
|
+
password: Password/token for authentication (optional).
|
|
54
|
+
"""
|
|
55
|
+
self.registry = registry
|
|
56
|
+
self.image = image
|
|
57
|
+
self.tag = tag
|
|
58
|
+
self.secure = secure
|
|
59
|
+
self.username = username
|
|
60
|
+
self.password = password
|
|
61
|
+
|
|
62
|
+
self._cached_auth = None
|
|
63
|
+
self._moniker = 'https' if secure else 'http'
|
|
64
|
+
|
|
65
|
+
self._config_digest = None
|
|
66
|
+
self._config_size = None
|
|
67
|
+
self._layers = []
|
|
68
|
+
|
|
69
|
+
def _request(self, method, url, headers=None, data=None, stream=False):
|
|
70
|
+
"""Make an authenticated request to the registry."""
|
|
71
|
+
if not headers:
|
|
72
|
+
headers = {}
|
|
73
|
+
|
|
74
|
+
headers['User-Agent'] = util.get_user_agent()
|
|
75
|
+
|
|
76
|
+
if self._cached_auth:
|
|
77
|
+
headers['Authorization'] = 'Bearer %s' % self._cached_auth
|
|
78
|
+
|
|
79
|
+
r = requests.request(method, url, headers=headers, data=data,
|
|
80
|
+
stream=stream)
|
|
81
|
+
|
|
82
|
+
if r.status_code == 401:
|
|
83
|
+
auth_header = r.headers.get('Www-Authenticate', '')
|
|
84
|
+
auth_re = re.compile(r'Bearer realm="([^"]*)",service="([^"]*)"')
|
|
85
|
+
m = auth_re.match(auth_header)
|
|
86
|
+
if m:
|
|
87
|
+
scope = 'repository:%s:pull,push' % self.image
|
|
88
|
+
auth_url = '%s?service=%s&scope=%s' % (m.group(1), m.group(2),
|
|
89
|
+
scope)
|
|
90
|
+
if self.username and self.password:
|
|
91
|
+
auth_r = requests.get(auth_url,
|
|
92
|
+
auth=(self.username, self.password))
|
|
93
|
+
else:
|
|
94
|
+
auth_r = requests.get(auth_url)
|
|
95
|
+
|
|
96
|
+
if auth_r.status_code == 200:
|
|
97
|
+
token = auth_r.json().get('token')
|
|
98
|
+
self._cached_auth = token
|
|
99
|
+
headers['Authorization'] = 'Bearer %s' % token
|
|
100
|
+
|
|
101
|
+
r = requests.request(method, url, headers=headers,
|
|
102
|
+
data=data, stream=stream)
|
|
103
|
+
|
|
104
|
+
return r
|
|
105
|
+
|
|
106
|
+
def _blob_exists(self, digest):
|
|
107
|
+
"""Check if a blob already exists in the registry."""
|
|
108
|
+
url = '%s://%s/v2/%s/blobs/%s' % (self._moniker, self.registry,
|
|
109
|
+
self.image, digest)
|
|
110
|
+
r = self._request('HEAD', url)
|
|
111
|
+
return r.status_code == 200
|
|
112
|
+
|
|
113
|
+
def _upload_blob(self, digest, data, size):
|
|
114
|
+
"""Upload a blob to the registry.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
digest: The sha256 digest of the blob (e.g., 'sha256:abc123...').
|
|
118
|
+
data: File-like object containing the blob data.
|
|
119
|
+
size: Size of the blob in bytes.
|
|
120
|
+
"""
|
|
121
|
+
if self._blob_exists(digest):
|
|
122
|
+
LOG.info('Blob %s already exists, skipping upload' % digest[:19])
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
LOG.info('Uploading blob %s (%d bytes)' % (digest[:19], size))
|
|
126
|
+
|
|
127
|
+
url = '%s://%s/v2/%s/blobs/uploads/' % (self._moniker, self.registry,
|
|
128
|
+
self.image)
|
|
129
|
+
r = self._request('POST', url)
|
|
130
|
+
|
|
131
|
+
if r.status_code not in (200, 202):
|
|
132
|
+
raise Exception('Failed to initiate blob upload: %d %s'
|
|
133
|
+
% (r.status_code, r.text))
|
|
134
|
+
|
|
135
|
+
location = r.headers.get('Location')
|
|
136
|
+
if not location:
|
|
137
|
+
raise Exception('No Location header in upload response')
|
|
138
|
+
|
|
139
|
+
if not location.startswith('http'):
|
|
140
|
+
location = '%s://%s%s' % (self._moniker, self.registry, location)
|
|
141
|
+
|
|
142
|
+
if '?' in location:
|
|
143
|
+
upload_url = '%s&digest=%s' % (location, digest)
|
|
144
|
+
else:
|
|
145
|
+
upload_url = '%s?digest=%s' % (location, digest)
|
|
146
|
+
|
|
147
|
+
data.seek(0)
|
|
148
|
+
r = self._request('PUT', upload_url,
|
|
149
|
+
headers={'Content-Type': 'application/octet-stream',
|
|
150
|
+
'Content-Length': str(size)},
|
|
151
|
+
data=data)
|
|
152
|
+
|
|
153
|
+
if r.status_code not in (200, 201, 202):
|
|
154
|
+
raise Exception('Failed to upload blob: %d %s'
|
|
155
|
+
% (r.status_code, r.text))
|
|
156
|
+
|
|
157
|
+
LOG.info('Blob uploaded successfully')
|
|
158
|
+
|
|
159
|
+
def fetch_callback(self, digest):
|
|
160
|
+
"""Always fetch all layers for pushing."""
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
def process_image_element(self, element_type, name, data):
|
|
164
|
+
"""Process an image element, uploading it to the registry."""
|
|
165
|
+
if element_type == constants.CONFIG_FILE and data is not None:
|
|
166
|
+
LOG.info('Processing config file')
|
|
167
|
+
|
|
168
|
+
data.seek(0)
|
|
169
|
+
config_data = data.read()
|
|
170
|
+
|
|
171
|
+
h = hashlib.sha256()
|
|
172
|
+
h.update(config_data)
|
|
173
|
+
self._config_digest = 'sha256:%s' % h.hexdigest()
|
|
174
|
+
self._config_size = len(config_data)
|
|
175
|
+
|
|
176
|
+
self._upload_blob(self._config_digest, io.BytesIO(config_data),
|
|
177
|
+
self._config_size)
|
|
178
|
+
|
|
179
|
+
elif element_type == constants.IMAGE_LAYER and data is not None:
|
|
180
|
+
LOG.info('Processing layer %s' % name)
|
|
181
|
+
|
|
182
|
+
data.seek(0)
|
|
183
|
+
layer_data = data.read()
|
|
184
|
+
|
|
185
|
+
compressed = io.BytesIO()
|
|
186
|
+
with gzip.GzipFile(fileobj=compressed, mode='wb') as gz:
|
|
187
|
+
gz.write(layer_data)
|
|
188
|
+
compressed.seek(0)
|
|
189
|
+
compressed_data = compressed.read()
|
|
190
|
+
|
|
191
|
+
h = hashlib.sha256()
|
|
192
|
+
h.update(compressed_data)
|
|
193
|
+
layer_digest = 'sha256:%s' % h.hexdigest()
|
|
194
|
+
layer_size = len(compressed_data)
|
|
195
|
+
|
|
196
|
+
self._upload_blob(layer_digest, io.BytesIO(compressed_data),
|
|
197
|
+
layer_size)
|
|
198
|
+
|
|
199
|
+
self._layers.append({
|
|
200
|
+
'mediaType': 'application/vnd.docker.image.rootfs.diff.tar.gzip',
|
|
201
|
+
'size': layer_size,
|
|
202
|
+
'digest': layer_digest
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
def finalize(self):
|
|
206
|
+
"""Push the image manifest to the registry."""
|
|
207
|
+
if not self._config_digest:
|
|
208
|
+
raise Exception('No config file was processed')
|
|
209
|
+
|
|
210
|
+
LOG.info('Pushing manifest for %s:%s' % (self.image, self.tag))
|
|
211
|
+
|
|
212
|
+
manifest = {
|
|
213
|
+
'schemaVersion': 2,
|
|
214
|
+
'mediaType': 'application/vnd.docker.distribution.manifest.v2+json',
|
|
215
|
+
'config': {
|
|
216
|
+
'mediaType': 'application/vnd.docker.container.image.v1+json',
|
|
217
|
+
'size': self._config_size,
|
|
218
|
+
'digest': self._config_digest
|
|
219
|
+
},
|
|
220
|
+
'layers': self._layers
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
manifest_json = json.dumps(manifest, separators=(',', ':'))
|
|
224
|
+
|
|
225
|
+
url = '%s://%s/v2/%s/manifests/%s' % (self._moniker, self.registry,
|
|
226
|
+
self.image, self.tag)
|
|
227
|
+
r = self._request(
|
|
228
|
+
'PUT', url,
|
|
229
|
+
headers={
|
|
230
|
+
'Content-Type':
|
|
231
|
+
'application/vnd.docker.distribution.manifest.v2+json'
|
|
232
|
+
},
|
|
233
|
+
data=manifest_json.encode('utf-8'))
|
|
234
|
+
|
|
235
|
+
if r.status_code not in (200, 201, 202):
|
|
236
|
+
raise Exception('Failed to push manifest: %d %s'
|
|
237
|
+
% (r.status_code, r.text))
|
|
238
|
+
|
|
239
|
+
LOG.info('Image pushed successfully: %s/%s:%s'
|
|
240
|
+
% (self.registry, self.image, self.tag))
|
|
@@ -5,18 +5,33 @@ import os
|
|
|
5
5
|
import tarfile
|
|
6
6
|
|
|
7
7
|
from occystrap import constants
|
|
8
|
+
from occystrap.outputs.base import ImageOutput
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
LOG = logging.getLogger(__name__)
|
|
11
12
|
LOG.setLevel(logging.INFO)
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
# This code creates v1.2 format image tarballs.
|
|
16
|
+
# v1.2 is documented at https://github.com/moby/docker-image-spec/blob/v1.2.0/v1.2.md
|
|
17
|
+
# v2 is documented at https://github.com/opencontainers/image-spec/blob/main/
|
|
18
|
+
|
|
19
|
+
class TarWriter(ImageOutput):
|
|
20
|
+
"""Creates docker-loadable tarballs in v1.2 format.
|
|
21
|
+
|
|
22
|
+
To normalize timestamps for reproducible builds, use the
|
|
23
|
+
TimestampNormalizer filter before this output.
|
|
24
|
+
|
|
25
|
+
Uses USTAR format for the outer tarball which contains only short paths
|
|
26
|
+
(SHA256 hashes and small filenames), avoiding PAX extended headers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
15
29
|
def __init__(self, image, tag, image_path):
|
|
16
30
|
self.image = image
|
|
17
31
|
self.tag = tag
|
|
18
32
|
self.image_path = image_path
|
|
19
|
-
self.image_tar = tarfile.open(image_path, 'w'
|
|
33
|
+
self.image_tar = tarfile.open(image_path, 'w',
|
|
34
|
+
format=tarfile.USTAR_FORMAT)
|
|
20
35
|
|
|
21
36
|
self.tar_manifest = [{
|
|
22
37
|
'Layers': [],
|
|
@@ -53,3 +68,4 @@ class TarWriter(object):
|
|
|
53
68
|
ti = tarfile.TarInfo('manifest.json')
|
|
54
69
|
ti.size = len(encoded_manifest)
|
|
55
70
|
self.image_tar.addfile(ti, io.BytesIO(encoded_manifest))
|
|
71
|
+
self.image_tar.close()
|