occystrap 0.2.0__py3-none-any.whl → 0.4.0__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/common.py +40 -0
- occystrap/constants.py +184 -0
- occystrap/docker_registry.py +75 -24
- occystrap/main.py +69 -10
- occystrap/output_directory.py +52 -30
- occystrap/output_mounts.py +155 -0
- occystrap/output_ocibundle.py +53 -0
- occystrap/output_tarfile.py +0 -5
- occystrap/util.py +9 -1
- {occystrap-0.2.0.dist-info → occystrap-0.4.0.dist-info}/METADATA +30 -1
- occystrap-0.4.0.dist-info/RECORD +20 -0
- occystrap-0.4.0.dist-info/pbr.json +1 -0
- occystrap-0.2.0.dist-info/RECORD +0 -17
- occystrap-0.2.0.dist-info/pbr.json +0 -1
- {occystrap-0.2.0.dist-info → occystrap-0.4.0.dist-info}/AUTHORS +0 -0
- {occystrap-0.2.0.dist-info → occystrap-0.4.0.dist-info}/LICENSE +0 -0
- {occystrap-0.2.0.dist-info → occystrap-0.4.0.dist-info}/WHEEL +0 -0
- {occystrap-0.2.0.dist-info → occystrap-0.4.0.dist-info}/entry_points.txt +0 -0
- {occystrap-0.2.0.dist-info → occystrap-0.4.0.dist-info}/top_level.txt +0 -0
occystrap/common.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from occystrap.constants import RUNC_SPEC_TEMPLATE
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def write_container_config(container_config_filename, runtime_config_filename,
|
|
7
|
+
container_template=RUNC_SPEC_TEMPLATE,
|
|
8
|
+
container_values=None):
|
|
9
|
+
if not container_values:
|
|
10
|
+
container_values = {}
|
|
11
|
+
|
|
12
|
+
# Read the container config
|
|
13
|
+
with open(container_config_filename) as f:
|
|
14
|
+
image_conf = json.loads(f.read())
|
|
15
|
+
|
|
16
|
+
# Write a runc specification for the container
|
|
17
|
+
container_conf = json.loads(container_template)
|
|
18
|
+
|
|
19
|
+
container_conf['process']['terminal'] = True
|
|
20
|
+
cwd = image_conf['config']['WorkingDir']
|
|
21
|
+
if cwd == '':
|
|
22
|
+
cwd = '/'
|
|
23
|
+
container_conf['process']['cwd'] = cwd
|
|
24
|
+
|
|
25
|
+
entrypoint = image_conf['config'].get('Entrypoint', [])
|
|
26
|
+
if not entrypoint:
|
|
27
|
+
entrypoint = []
|
|
28
|
+
cmd = image_conf['config'].get('Cmd', [])
|
|
29
|
+
if cmd:
|
|
30
|
+
entrypoint.extend(cmd)
|
|
31
|
+
container_conf['process']['args'] = entrypoint
|
|
32
|
+
|
|
33
|
+
# terminal = false means "pass through existing file descriptors"
|
|
34
|
+
container_conf['process']['terminal'] = False
|
|
35
|
+
|
|
36
|
+
container_conf['hostname'] = container_values.get(
|
|
37
|
+
'hostname', 'occystrap')
|
|
38
|
+
|
|
39
|
+
with open(runtime_config_filename, 'w') as f:
|
|
40
|
+
f.write(json.dumps(container_conf, indent=4, sort_keys=True))
|
occystrap/constants.py
CHANGED
|
@@ -1,2 +1,186 @@
|
|
|
1
1
|
CONFIG_FILE = 'config_file'
|
|
2
2
|
IMAGE_LAYER = 'image_layer'
|
|
3
|
+
|
|
4
|
+
RUNC_SPEC_TEMPLATE = """{
|
|
5
|
+
"ociVersion": "1.0.2-dev",
|
|
6
|
+
"process": {
|
|
7
|
+
"terminal": false,
|
|
8
|
+
"user": {
|
|
9
|
+
"uid": 0,
|
|
10
|
+
"gid": 0
|
|
11
|
+
},
|
|
12
|
+
"args": [
|
|
13
|
+
"sh"
|
|
14
|
+
],
|
|
15
|
+
"env": [
|
|
16
|
+
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
17
|
+
"TERM=xterm"
|
|
18
|
+
],
|
|
19
|
+
"cwd": "/",
|
|
20
|
+
"capabilities": {
|
|
21
|
+
"bounding": [
|
|
22
|
+
"CAP_AUDIT_WRITE",
|
|
23
|
+
"CAP_KILL",
|
|
24
|
+
"CAP_NET_BIND_SERVICE"
|
|
25
|
+
],
|
|
26
|
+
"effective": [
|
|
27
|
+
"CAP_AUDIT_WRITE",
|
|
28
|
+
"CAP_KILL",
|
|
29
|
+
"CAP_NET_BIND_SERVICE"
|
|
30
|
+
],
|
|
31
|
+
"inheritable": [
|
|
32
|
+
"CAP_AUDIT_WRITE",
|
|
33
|
+
"CAP_KILL",
|
|
34
|
+
"CAP_NET_BIND_SERVICE"
|
|
35
|
+
],
|
|
36
|
+
"permitted": [
|
|
37
|
+
"CAP_AUDIT_WRITE",
|
|
38
|
+
"CAP_KILL",
|
|
39
|
+
"CAP_NET_BIND_SERVICE"
|
|
40
|
+
],
|
|
41
|
+
"ambient": [
|
|
42
|
+
"CAP_AUDIT_WRITE",
|
|
43
|
+
"CAP_KILL",
|
|
44
|
+
"CAP_NET_BIND_SERVICE"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"rlimits": [
|
|
48
|
+
{
|
|
49
|
+
"type": "RLIMIT_NOFILE",
|
|
50
|
+
"hard": 1024,
|
|
51
|
+
"soft": 1024
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"noNewPrivileges": true
|
|
55
|
+
},
|
|
56
|
+
"root": {
|
|
57
|
+
"path": "rootfs",
|
|
58
|
+
"readonly": true
|
|
59
|
+
},
|
|
60
|
+
"hostname": "runc",
|
|
61
|
+
"mounts": [
|
|
62
|
+
{
|
|
63
|
+
"destination": "/proc",
|
|
64
|
+
"type": "proc",
|
|
65
|
+
"source": "proc"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"destination": "/dev",
|
|
69
|
+
"type": "tmpfs",
|
|
70
|
+
"source": "tmpfs",
|
|
71
|
+
"options": [
|
|
72
|
+
"nosuid",
|
|
73
|
+
"strictatime",
|
|
74
|
+
"mode=755",
|
|
75
|
+
"size=65536k"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"destination": "/dev/pts",
|
|
80
|
+
"type": "devpts",
|
|
81
|
+
"source": "devpts",
|
|
82
|
+
"options": [
|
|
83
|
+
"nosuid",
|
|
84
|
+
"noexec",
|
|
85
|
+
"newinstance",
|
|
86
|
+
"ptmxmode=0666",
|
|
87
|
+
"mode=0620",
|
|
88
|
+
"gid=5"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"destination": "/dev/shm",
|
|
93
|
+
"type": "tmpfs",
|
|
94
|
+
"source": "shm",
|
|
95
|
+
"options": [
|
|
96
|
+
"nosuid",
|
|
97
|
+
"noexec",
|
|
98
|
+
"nodev",
|
|
99
|
+
"mode=1777",
|
|
100
|
+
"size=65536k"
|
|
101
|
+
]
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"destination": "/dev/mqueue",
|
|
105
|
+
"type": "mqueue",
|
|
106
|
+
"source": "mqueue",
|
|
107
|
+
"options": [
|
|
108
|
+
"nosuid",
|
|
109
|
+
"noexec",
|
|
110
|
+
"nodev"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"destination": "/sys",
|
|
115
|
+
"type": "sysfs",
|
|
116
|
+
"source": "sysfs",
|
|
117
|
+
"options": [
|
|
118
|
+
"nosuid",
|
|
119
|
+
"noexec",
|
|
120
|
+
"nodev",
|
|
121
|
+
"ro"
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"destination": "/sys/fs/cgroup",
|
|
126
|
+
"type": "cgroup",
|
|
127
|
+
"source": "cgroup",
|
|
128
|
+
"options": [
|
|
129
|
+
"nosuid",
|
|
130
|
+
"noexec",
|
|
131
|
+
"nodev",
|
|
132
|
+
"relatime",
|
|
133
|
+
"ro"
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"linux": {
|
|
138
|
+
"resources": {
|
|
139
|
+
"devices": [
|
|
140
|
+
{
|
|
141
|
+
"allow": false,
|
|
142
|
+
"access": "rwm"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
"namespaces": [
|
|
147
|
+
{
|
|
148
|
+
"type": "pid"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"type": "network"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"type": "ipc"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"type": "uts"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"type": "mount"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "cgroup"
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
"maskedPaths": [
|
|
167
|
+
"/proc/acpi",
|
|
168
|
+
"/proc/asound",
|
|
169
|
+
"/proc/kcore",
|
|
170
|
+
"/proc/keys",
|
|
171
|
+
"/proc/latency_stats",
|
|
172
|
+
"/proc/timer_list",
|
|
173
|
+
"/proc/timer_stats",
|
|
174
|
+
"/proc/sched_debug",
|
|
175
|
+
"/sys/firmware",
|
|
176
|
+
"/proc/scsi"
|
|
177
|
+
],
|
|
178
|
+
"readonlyPaths": [
|
|
179
|
+
"/proc/bus",
|
|
180
|
+
"/proc/fs",
|
|
181
|
+
"/proc/irq",
|
|
182
|
+
"/proc/sys",
|
|
183
|
+
"/proc/sysrq-trigger"
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
}"""
|
occystrap/docker_registry.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# A simple implementation of a docker registry client. Fetches an image to a tarball.
|
|
2
2
|
# With a big nod to https://github.com/NotGlop/docker-drag/blob/master/docker_pull.py
|
|
3
3
|
|
|
4
|
+
# https://docs.docker.com/registry/spec/manifest-v2-2/ documents the image manifest
|
|
5
|
+
# format, noting that the response format you get back varies based on what you have
|
|
6
|
+
# in your accept header for the request.
|
|
7
|
+
|
|
8
|
+
# https://github.com/opencontainers/image-spec/blob/main/media-types.md documents
|
|
9
|
+
# the new OCI mime types.
|
|
10
|
+
|
|
4
11
|
import hashlib
|
|
5
12
|
import io
|
|
6
|
-
import json
|
|
7
13
|
import logging
|
|
8
14
|
import os
|
|
9
15
|
import re
|
|
10
16
|
import sys
|
|
11
|
-
import tarfile
|
|
12
17
|
import tempfile
|
|
13
18
|
import zlib
|
|
14
19
|
|
|
@@ -18,7 +23,7 @@ from occystrap import util
|
|
|
18
23
|
LOG = logging.getLogger(__name__)
|
|
19
24
|
LOG.setLevel(logging.INFO)
|
|
20
25
|
|
|
21
|
-
DELETED_FILE_RE = re.compile('.*/\.wh\.(.*)$')
|
|
26
|
+
DELETED_FILE_RE = re.compile(r'.*/\.wh\.(.*)$')
|
|
22
27
|
|
|
23
28
|
|
|
24
29
|
def always_fetch():
|
|
@@ -26,10 +31,16 @@ def always_fetch():
|
|
|
26
31
|
|
|
27
32
|
|
|
28
33
|
class Image(object):
|
|
29
|
-
def __init__(self, registry, image, tag
|
|
34
|
+
def __init__(self, registry, image, tag, os='linux', architecture='amd64', variant='',
|
|
35
|
+
secure=True):
|
|
30
36
|
self.registry = registry
|
|
31
37
|
self.image = image
|
|
32
38
|
self.tag = tag
|
|
39
|
+
self.os = os
|
|
40
|
+
self.architecture = architecture
|
|
41
|
+
self.variant = variant
|
|
42
|
+
self.secure = secure
|
|
43
|
+
|
|
33
44
|
self._cached_auth = None
|
|
34
45
|
|
|
35
46
|
def request_url(self, method, url, headers=None, data=None, stream=False):
|
|
@@ -58,36 +69,82 @@ class Image(object):
|
|
|
58
69
|
|
|
59
70
|
def fetch(self, fetch_callback=always_fetch):
|
|
60
71
|
LOG.info('Fetching manifest')
|
|
72
|
+
moniker = 'https'
|
|
73
|
+
if not self.secure:
|
|
74
|
+
moniker = 'http'
|
|
75
|
+
|
|
61
76
|
r = self.request_url(
|
|
62
77
|
'GET',
|
|
63
|
-
'
|
|
78
|
+
'%(moniker)s://%(registry)s/v2/%(image)s/manifests/%(tag)s'
|
|
64
79
|
% {
|
|
80
|
+
'moniker': moniker,
|
|
65
81
|
'registry': self.registry,
|
|
66
82
|
'image': self.image,
|
|
67
83
|
'tag': self.tag
|
|
68
84
|
},
|
|
69
|
-
headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'
|
|
70
|
-
|
|
85
|
+
headers={'Accept': ('application/vnd.docker.distribution.manifest.v2+json,'
|
|
86
|
+
'application/vnd.docker.distribution.manifest.list.v2+json')})
|
|
87
|
+
|
|
88
|
+
config_digest = None
|
|
89
|
+
if r.headers['Content-Type'] == 'application/vnd.docker.distribution.manifest.v2+json':
|
|
90
|
+
manifest = r.json()
|
|
91
|
+
config_digest = manifest['config']['digest']
|
|
92
|
+
elif r.headers['Content-Type'] in [
|
|
93
|
+
'application/vnd.docker.distribution.manifest.list.v2+json',
|
|
94
|
+
'application/vnd.oci.image.index.v1+json']:
|
|
95
|
+
for m in r.json()['manifests']:
|
|
96
|
+
if 'variant' in m['platform']:
|
|
97
|
+
LOG.info('Found manifest for %s on %s %s'
|
|
98
|
+
% (m['platform']['os'], m['platform']['architecture'],
|
|
99
|
+
m['platform']['variant']))
|
|
100
|
+
else:
|
|
101
|
+
LOG.info('Found manifest for %s on %s'
|
|
102
|
+
% (m['platform']['os'], m['platform']['architecture']))
|
|
103
|
+
|
|
104
|
+
if (m['platform']['os'] == self.os and
|
|
105
|
+
m['platform']['architecture'] == self.architecture and
|
|
106
|
+
m['platform'].get('variant', '') == self.variant):
|
|
107
|
+
LOG.info('Fetching matching manifest')
|
|
108
|
+
r = self.request_url(
|
|
109
|
+
'GET',
|
|
110
|
+
'%(moniker)s://%(registry)s/v2/%(image)s/manifests/%(tag)s'
|
|
111
|
+
% {
|
|
112
|
+
'moniker': moniker,
|
|
113
|
+
'registry': self.registry,
|
|
114
|
+
'image': self.image,
|
|
115
|
+
'tag': m['digest']
|
|
116
|
+
},
|
|
117
|
+
headers={'Accept': ('application/vnd.docker.distribution.manifest.v2+json, '
|
|
118
|
+
'application/vnd.oci.image.manifest.v1+json')})
|
|
119
|
+
manifest = r.json()
|
|
120
|
+
config_digest = manifest['config']['digest']
|
|
121
|
+
|
|
122
|
+
if not config_digest:
|
|
123
|
+
raise Exception('Could not find a matching manifest for this '
|
|
124
|
+
'os / architecture / variant')
|
|
125
|
+
else:
|
|
126
|
+
raise Exception('Unknown manifest content type %s!' %
|
|
127
|
+
r.headers['Content-Type'])
|
|
71
128
|
|
|
72
129
|
LOG.info('Fetching config file')
|
|
73
130
|
r = self.request_url(
|
|
74
131
|
'GET',
|
|
75
|
-
'
|
|
132
|
+
'%(moniker)s://%(registry)s/v2/%(image)s/blobs/%(config)s'
|
|
76
133
|
% {
|
|
134
|
+
'moniker': moniker,
|
|
77
135
|
'registry': self.registry,
|
|
78
136
|
'image': self.image,
|
|
79
|
-
'config':
|
|
137
|
+
'config': config_digest
|
|
80
138
|
})
|
|
81
139
|
config = r.content
|
|
82
140
|
h = hashlib.sha256()
|
|
83
141
|
h.update(config)
|
|
84
|
-
if h.hexdigest() !=
|
|
142
|
+
if h.hexdigest() != config_digest.split(':')[1]:
|
|
85
143
|
LOG.error('Hash verification failed for image config blob (%s vs %s)'
|
|
86
|
-
% (
|
|
144
|
+
% (config_digest.split(':')[1], h.hexdigest()))
|
|
87
145
|
sys.exit(1)
|
|
88
146
|
|
|
89
|
-
config_filename = ('%s.json'
|
|
90
|
-
% manifest['config']['digest'].split(':')[1])
|
|
147
|
+
config_filename = ('%s.json' % config_digest.split(':')[1])
|
|
91
148
|
yield (constants.CONFIG_FILE, config_filename,
|
|
92
149
|
io.BytesIO(config))
|
|
93
150
|
|
|
@@ -96,15 +153,16 @@ class Image(object):
|
|
|
96
153
|
layer_filename = layer['digest'].split(':')[1]
|
|
97
154
|
if not fetch_callback(layer_filename):
|
|
98
155
|
LOG.info('Fetch callback says skip layer %s' % layer['digest'])
|
|
99
|
-
yield(constants.IMAGE_LAYER, layer_filename, None)
|
|
156
|
+
yield (constants.IMAGE_LAYER, layer_filename, None)
|
|
100
157
|
continue
|
|
101
158
|
|
|
102
159
|
LOG.info('Fetching layer %s (%d bytes)'
|
|
103
160
|
% (layer['digest'], layer['size']))
|
|
104
161
|
r = self.request_url(
|
|
105
162
|
'GET',
|
|
106
|
-
'
|
|
163
|
+
'%(moniker)s://%(registry)s/v2/%(image)s/blobs/%(layer)s'
|
|
107
164
|
% {
|
|
165
|
+
'moniker': moniker,
|
|
108
166
|
'registry': self.registry,
|
|
109
167
|
'image': self.image,
|
|
110
168
|
'layer': layer['digest']
|
|
@@ -128,18 +186,11 @@ class Image(object):
|
|
|
128
186
|
|
|
129
187
|
if h.hexdigest() != layer_filename:
|
|
130
188
|
LOG.error('Hash verification failed for layer (%s vs %s)'
|
|
131
|
-
% (
|
|
189
|
+
% (layer_filename, h.hexdigest()))
|
|
132
190
|
sys.exit(1)
|
|
133
191
|
|
|
134
|
-
with tarfile.open(tf.name) as layer:
|
|
135
|
-
for mem in layer.getmembers():
|
|
136
|
-
m = DELETED_FILE_RE.match(mem.name)
|
|
137
|
-
if m:
|
|
138
|
-
LOG.info('Layer tarball contains deleted file: %s'
|
|
139
|
-
% mem.name)
|
|
140
|
-
|
|
141
192
|
with open(tf.name, 'rb') as f:
|
|
142
|
-
yield(constants.IMAGE_LAYER, layer_filename, f)
|
|
193
|
+
yield (constants.IMAGE_LAYER, layer_filename, f)
|
|
143
194
|
|
|
144
195
|
finally:
|
|
145
196
|
os.unlink(tf.name)
|
occystrap/main.py
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
import click
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
4
|
+
from shakenfist_utilities import logs
|
|
5
|
+
import sys
|
|
3
6
|
|
|
4
7
|
from occystrap import docker_registry
|
|
5
8
|
from occystrap import output_directory
|
|
9
|
+
from occystrap import output_mounts
|
|
10
|
+
from occystrap import output_ocibundle
|
|
6
11
|
from occystrap import output_tarfile
|
|
7
12
|
|
|
8
|
-
logging.basicConfig(level=logging.INFO)
|
|
9
13
|
|
|
10
|
-
LOG =
|
|
11
|
-
LOG.setLevel(logging.INFO)
|
|
14
|
+
LOG = logs.setup_console(__name__)
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
@click.group()
|
|
15
18
|
@click.option('--verbose', is_flag=True)
|
|
19
|
+
@click.option('--os', default='linux')
|
|
20
|
+
@click.option('--architecture', default='amd64')
|
|
21
|
+
@click.option('--variant', default='')
|
|
16
22
|
@click.pass_context
|
|
17
|
-
def cli(ctx, verbose=None):
|
|
23
|
+
def cli(ctx, verbose=None, os=None, architecture=None, variant=None):
|
|
18
24
|
if verbose:
|
|
19
25
|
logging.basicConfig(level=logging.DEBUG)
|
|
20
26
|
LOG.setLevel(logging.DEBUG)
|
|
21
27
|
|
|
28
|
+
if not ctx.obj:
|
|
29
|
+
ctx.obj = {}
|
|
30
|
+
ctx.obj['OS'] = os
|
|
31
|
+
ctx.obj['ARCHITECTURE'] = architecture
|
|
32
|
+
ctx.obj['VARIANT'] = variant
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
def _fetch(registry, image, tag, output, os, architecture, variant, secure=True):
|
|
36
|
+
img = docker_registry.Image(
|
|
37
|
+
registry, image, tag, os, architecture, variant, secure=secure)
|
|
25
38
|
for image_element in img.fetch(fetch_callback=output.fetch_callback):
|
|
26
39
|
output.process_image_element(*image_element)
|
|
27
40
|
output.finalize()
|
|
@@ -34,11 +47,14 @@ def _fetch(registry, image, tag, output):
|
|
|
34
47
|
@click.argument('path')
|
|
35
48
|
@click.option('--use-unique-names', is_flag=True)
|
|
36
49
|
@click.option('--expand', is_flag=True)
|
|
50
|
+
@click.option('--insecure', is_flag=True, default=False)
|
|
37
51
|
@click.pass_context
|
|
38
|
-
def fetch_to_extracted(ctx, registry, image, tag, path, use_unique_names,
|
|
52
|
+
def fetch_to_extracted(ctx, registry, image, tag, path, use_unique_names,
|
|
53
|
+
expand, insecure):
|
|
39
54
|
d = output_directory.DirWriter(
|
|
40
55
|
image, tag, path, unique_names=use_unique_names, expand=expand)
|
|
41
|
-
_fetch(registry, image, tag, d
|
|
56
|
+
_fetch(registry, image, tag, d, ctx.obj['OS'], ctx.obj['ARCHITECTURE'],
|
|
57
|
+
ctx.obj['VARIANT'], secure=(not insecure))
|
|
42
58
|
|
|
43
59
|
if expand:
|
|
44
60
|
d.write_bundle()
|
|
@@ -47,20 +63,63 @@ def fetch_to_extracted(ctx, registry, image, tag, path, use_unique_names, expand
|
|
|
47
63
|
cli.add_command(fetch_to_extracted)
|
|
48
64
|
|
|
49
65
|
|
|
66
|
+
@click.command()
|
|
67
|
+
@click.argument('registry')
|
|
68
|
+
@click.argument('image')
|
|
69
|
+
@click.argument('tag')
|
|
70
|
+
@click.argument('path')
|
|
71
|
+
@click.option('--insecure', is_flag=True, default=False)
|
|
72
|
+
@click.pass_context
|
|
73
|
+
def fetch_to_oci(ctx, registry, image, tag, path, insecure):
|
|
74
|
+
d = output_ocibundle.OCIBundleWriter(image, tag, path)
|
|
75
|
+
_fetch(registry, image, tag, d, ctx.obj['OS'], ctx.obj['ARCHITECTURE'],
|
|
76
|
+
ctx.obj['VARIANT'], secure=(not insecure))
|
|
77
|
+
d.write_bundle()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
cli.add_command(fetch_to_oci)
|
|
81
|
+
|
|
82
|
+
|
|
50
83
|
@click.command()
|
|
51
84
|
@click.argument('registry')
|
|
52
85
|
@click.argument('image')
|
|
53
86
|
@click.argument('tag')
|
|
54
87
|
@click.argument('tarfile')
|
|
88
|
+
@click.option('--insecure', is_flag=True, default=False)
|
|
55
89
|
@click.pass_context
|
|
56
|
-
def fetch_to_tarfile(ctx, registry, image, tag, tarfile):
|
|
90
|
+
def fetch_to_tarfile(ctx, registry, image, tag, tarfile, insecure):
|
|
57
91
|
tar = output_tarfile.TarWriter(image, tag, tarfile)
|
|
58
|
-
_fetch(registry, image, tag, tar
|
|
92
|
+
_fetch(registry, image, tag, tar, ctx.obj['OS'], ctx.obj['ARCHITECTURE'],
|
|
93
|
+
ctx.obj['VARIANT'], secure=(not insecure))
|
|
59
94
|
|
|
60
95
|
|
|
61
96
|
cli.add_command(fetch_to_tarfile)
|
|
62
97
|
|
|
63
98
|
|
|
99
|
+
@click.command()
|
|
100
|
+
@click.argument('registry')
|
|
101
|
+
@click.argument('image')
|
|
102
|
+
@click.argument('tag')
|
|
103
|
+
@click.argument('path')
|
|
104
|
+
@click.option('--insecure', is_flag=True, default=False)
|
|
105
|
+
@click.pass_context
|
|
106
|
+
def fetch_to_mounts(ctx, registry, image, tag, path, insecure):
|
|
107
|
+
if not hasattr(os, 'setxattr'):
|
|
108
|
+
print('Sorry, your OS module implementation lacks setxattr')
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
if not hasattr(os, 'mknod'):
|
|
111
|
+
print('Sorry, your OS module implementation lacks mknod')
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
d = output_mounts.MountWriter(image, tag, path)
|
|
115
|
+
_fetch(registry, image, tag, d, ctx.obj['OS'], ctx.obj['ARCHITECTURE'],
|
|
116
|
+
ctx.obj['VARIANT'], secure=(not insecure))
|
|
117
|
+
d.write_bundle()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
cli.add_command(fetch_to_mounts)
|
|
121
|
+
|
|
122
|
+
|
|
64
123
|
@click.command()
|
|
65
124
|
@click.argument('path')
|
|
66
125
|
@click.argument('image')
|
occystrap/output_directory.py
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import hashlib
|
|
2
1
|
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
|
-
import prettytable
|
|
6
|
-
import sys
|
|
7
4
|
import tarfile
|
|
8
|
-
import zlib
|
|
9
5
|
|
|
10
6
|
from occystrap import constants
|
|
11
7
|
|
|
@@ -128,14 +124,13 @@ class DirWriter(object):
|
|
|
128
124
|
def _create_bundle_path(self, path):
|
|
129
125
|
d = self.bundle
|
|
130
126
|
for elem in path.split('/'):
|
|
131
|
-
if not
|
|
127
|
+
if elem not in d:
|
|
132
128
|
d[elem] = {}
|
|
133
129
|
d = d[elem]
|
|
134
130
|
return d
|
|
135
131
|
|
|
136
132
|
def fetch_callback(self, digest):
|
|
137
|
-
layer_file_in_dir = os.path.join(self.image_path,
|
|
138
|
-
digest, 'layer.tar')
|
|
133
|
+
layer_file_in_dir = os.path.join(self.image_path, digest, 'layer.tar')
|
|
139
134
|
LOG.info('Layer file is %s' % layer_file_in_dir)
|
|
140
135
|
return not os.path.exists(layer_file_in_dir)
|
|
141
136
|
|
|
@@ -168,16 +163,39 @@ class DirWriter(object):
|
|
|
168
163
|
# Build a in-memory map of the layout of the final image bundle
|
|
169
164
|
with tarfile.open(layer_file_in_dir) as layer:
|
|
170
165
|
for mem in layer.getmembers():
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
path = mem.name
|
|
167
|
+
dirname, filename = os.path.split(mem.name)
|
|
168
|
+
|
|
169
|
+
# Some light reading on how this works...
|
|
170
|
+
# https://github.com/opencontainers/image-spec/blob/main/layer.md#opaque-whiteout
|
|
171
|
+
if filename == '.wh..wh..opq':
|
|
172
|
+
# A deleted directory, but only for layers below
|
|
173
|
+
# this one.
|
|
174
|
+
for ent in self.bundle:
|
|
175
|
+
if (ent.startswith(dirname) and
|
|
176
|
+
self.bundle[ent][-1].tarpath != layer_file):
|
|
177
|
+
self.bundle[ent].append(
|
|
178
|
+
BundleDeletedFile(ent, layer_file, mem))
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
elif filename.startswith('.wh.'):
|
|
182
|
+
# A single deleted element, which might not be a
|
|
183
|
+
# file.
|
|
184
|
+
path = os.path.join(dirname, filename[4:])
|
|
185
|
+
if type(self.bundle[path][-1]) is BundleDirectory:
|
|
186
|
+
for ent in self.bundle:
|
|
187
|
+
if ent.startswith(path):
|
|
188
|
+
self.bundle[ent].append(
|
|
189
|
+
BundleDeletedFile(ent, layer_file, mem))
|
|
190
|
+
|
|
173
191
|
serialized = BundleDeletedFile(
|
|
174
|
-
|
|
192
|
+
path, layer_file, mem)
|
|
175
193
|
else:
|
|
176
194
|
serialized = TARFILE_TYPE_MAP[mem.type](
|
|
177
195
|
mem.name, layer_file, mem)
|
|
178
196
|
|
|
179
|
-
self.bundle.setdefault(
|
|
180
|
-
self.bundle[
|
|
197
|
+
self.bundle.setdefault(path, [])
|
|
198
|
+
self.bundle[path].append(serialized)
|
|
181
199
|
|
|
182
200
|
def _log_bundle(self):
|
|
183
201
|
savings = 0
|
|
@@ -185,13 +203,17 @@ class DirWriter(object):
|
|
|
185
203
|
for path in self.bundle:
|
|
186
204
|
versions = len(self.bundle[path])
|
|
187
205
|
if versions > 1:
|
|
188
|
-
|
|
206
|
+
path_savings = 0
|
|
207
|
+
LOG.info('Bundle path "%s" has %d versions'
|
|
189
208
|
% (path, versions))
|
|
190
209
|
for ver in self.bundle[path][:-1]:
|
|
191
|
-
|
|
210
|
+
path_savings += ver.size
|
|
211
|
+
if type(self.bundle[path][-1]) is BundleDeletedFile:
|
|
212
|
+
LOG.info('Bundle path "%s" final version is a deleted file, '
|
|
213
|
+
'which wasted %d bytes.' % (path, path_savings))
|
|
214
|
+
savings += path_savings
|
|
192
215
|
|
|
193
|
-
LOG.info('Flattening image would save %d bytes'
|
|
194
|
-
% savings)
|
|
216
|
+
LOG.info('Flattening image would save %d bytes' % savings)
|
|
195
217
|
|
|
196
218
|
def finalize(self):
|
|
197
219
|
if self.expand:
|
|
@@ -214,13 +236,7 @@ class DirWriter(object):
|
|
|
214
236
|
with open(catalog_path, 'w') as f:
|
|
215
237
|
f.write(json.dumps(c, indent=4, sort_keys=True))
|
|
216
238
|
|
|
217
|
-
def
|
|
218
|
-
LOG.info('Writing image bundle')
|
|
219
|
-
manifest_filename = self._manifest_filename()
|
|
220
|
-
manifest_path = os.path.join(self.image_path, manifest_filename)
|
|
221
|
-
if not os.path.exists(manifest_path):
|
|
222
|
-
os.makedirs(manifest_path)
|
|
223
|
-
|
|
239
|
+
def _extract_rootfs(self, rootfs_path):
|
|
224
240
|
# Reading tarfiles is expensive, as tarfile needs to scan the
|
|
225
241
|
# entire file to find the right entry. It builds a cache while
|
|
226
242
|
# doing this however, so performance improves if you access a
|
|
@@ -250,14 +266,20 @@ class DirWriter(object):
|
|
|
250
266
|
for tarpath in entities_by_layer:
|
|
251
267
|
with tarfile.open(os.path.join(self.image_path, tarpath)) as layer:
|
|
252
268
|
for ent in entities_by_layer[tarpath]:
|
|
253
|
-
|
|
254
|
-
layer.extract(ent.name, path=entdest)
|
|
269
|
+
layer.extract(ent.name, path=rootfs_path)
|
|
255
270
|
|
|
256
271
|
for tarpath in deferred_by_layer:
|
|
257
272
|
with tarfile.open(os.path.join(self.image_path, tarpath)) as layer:
|
|
258
273
|
for ent in deferred_by_layer[tarpath]:
|
|
259
|
-
|
|
260
|
-
|
|
274
|
+
layer.extract(ent.name, path=rootfs_path)
|
|
275
|
+
|
|
276
|
+
def write_bundle(self):
|
|
277
|
+
manifest_filename = self._manifest_filename()
|
|
278
|
+
manifest_path = os.path.join(self.image_path, manifest_filename)
|
|
279
|
+
if not os.path.exists(manifest_path):
|
|
280
|
+
os.makedirs(manifest_path)
|
|
281
|
+
LOG.info('Writing image bundle to %s' % manifest_path)
|
|
282
|
+
self._extract_rootfs(manifest_path)
|
|
261
283
|
|
|
262
284
|
|
|
263
285
|
class NoSuchImageException(Exception):
|
|
@@ -276,9 +298,9 @@ class DirReader(object):
|
|
|
276
298
|
with open(catalog_path, 'r') as f:
|
|
277
299
|
c = json.loads(f.read())
|
|
278
300
|
|
|
279
|
-
if
|
|
301
|
+
if self.image not in c:
|
|
280
302
|
raise NoSuchImageException(self.image)
|
|
281
|
-
if
|
|
303
|
+
if self.tag not in c[self.image]:
|
|
282
304
|
raise NoSuchImageException(self.image)
|
|
283
305
|
|
|
284
306
|
self.manifest_filename = c[self.image][self.tag]
|
|
@@ -289,7 +311,7 @@ class DirReader(object):
|
|
|
289
311
|
|
|
290
312
|
config_filename = manifest[0]['Config']
|
|
291
313
|
with open(os.path.join(self.path, config_filename), 'rb') as f:
|
|
292
|
-
yield(constants.CONFIG_FILE, config_filename, f)
|
|
314
|
+
yield (constants.CONFIG_FILE, config_filename, f)
|
|
293
315
|
|
|
294
316
|
for layer in manifest[0]['Layers']:
|
|
295
317
|
with open(os.path.join(self.path, layer), 'rb') as f:
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import stat
|
|
5
|
+
import tarfile
|
|
6
|
+
|
|
7
|
+
from occystrap import common
|
|
8
|
+
from occystrap import constants
|
|
9
|
+
from occystrap import util
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger(__name__)
|
|
13
|
+
LOG.setLevel(logging.INFO)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MountWriter(object):
|
|
17
|
+
def __init__(self, image, tag, image_path):
|
|
18
|
+
self.image = image
|
|
19
|
+
self.tag = tag
|
|
20
|
+
self.image_path = image_path
|
|
21
|
+
|
|
22
|
+
self.tar_manifest = [{
|
|
23
|
+
'Layers': [],
|
|
24
|
+
'RepoTags': ['%s:%s' % (self.image.split('/')[-1], self.tag)]
|
|
25
|
+
}]
|
|
26
|
+
|
|
27
|
+
self.bundle = {}
|
|
28
|
+
|
|
29
|
+
if not os.path.exists(self.image_path):
|
|
30
|
+
os.makedirs(self.image_path)
|
|
31
|
+
|
|
32
|
+
def _manifest_filename(self):
|
|
33
|
+
return 'manifest'
|
|
34
|
+
|
|
35
|
+
def fetch_callback(self, digest):
|
|
36
|
+
layer_file_in_dir = os.path.join(self.image_path, digest, 'layer.tar')
|
|
37
|
+
LOG.info('Layer file is %s' % layer_file_in_dir)
|
|
38
|
+
return not os.path.exists(layer_file_in_dir)
|
|
39
|
+
|
|
40
|
+
def process_image_element(self, element_type, name, data):
|
|
41
|
+
if element_type == constants.CONFIG_FILE:
|
|
42
|
+
with open(os.path.join(self.image_path, name), 'wb') as f:
|
|
43
|
+
d = json.loads(data.read())
|
|
44
|
+
f.write(json.dumps(d, indent=4, sort_keys=True).encode('ascii'))
|
|
45
|
+
self.tar_manifest[0]['Config'] = name
|
|
46
|
+
|
|
47
|
+
elif element_type == constants.IMAGE_LAYER:
|
|
48
|
+
layer_dir = os.path.join(self.image_path, name)
|
|
49
|
+
if not os.path.exists(layer_dir):
|
|
50
|
+
os.makedirs(layer_dir)
|
|
51
|
+
|
|
52
|
+
layer_file = os.path.join(name, 'layer.tar')
|
|
53
|
+
self.tar_manifest[0]['Layers'].append(layer_file)
|
|
54
|
+
|
|
55
|
+
layer_file_in_dir = os.path.join(self.image_path, layer_file)
|
|
56
|
+
if os.path.exists(layer_file_in_dir):
|
|
57
|
+
LOG.info('Skipping layer already in output directory')
|
|
58
|
+
else:
|
|
59
|
+
with open(layer_file_in_dir, 'wb') as f:
|
|
60
|
+
d = data.read(102400)
|
|
61
|
+
while d:
|
|
62
|
+
f.write(d)
|
|
63
|
+
d = data.read(102400)
|
|
64
|
+
|
|
65
|
+
layer_dir_in_dir = os.path.join(self.image_path, name, 'layer')
|
|
66
|
+
os.makedirs(layer_dir_in_dir)
|
|
67
|
+
with tarfile.open(layer_file_in_dir) as layer:
|
|
68
|
+
for mem in layer.getmembers():
|
|
69
|
+
dirname, filename = os.path.split(mem.name)
|
|
70
|
+
|
|
71
|
+
# Some light reading on how this works...
|
|
72
|
+
# https://www.madebymikal.com/interpreting-whiteout-files-in-docker-image-layers/
|
|
73
|
+
# https://github.com/opencontainers/image-spec/blob/main/layer.md#opaque-whiteout
|
|
74
|
+
if filename == '.wh..wh..opq':
|
|
75
|
+
# A deleted directory, but only for layers below
|
|
76
|
+
# this one.
|
|
77
|
+
os.setxattr(os.path.join(layer_dir_in_dir, dirname),
|
|
78
|
+
'trusted.overlay.opaque', b'y')
|
|
79
|
+
|
|
80
|
+
elif filename.startswith('.wh.'):
|
|
81
|
+
# A single deleted element, which might not be a
|
|
82
|
+
# file.
|
|
83
|
+
os.mknod(os.path.join(layer_dir_in_dir,
|
|
84
|
+
mem.name[4:]),
|
|
85
|
+
mode=stat.S_IFCHR, device=0)
|
|
86
|
+
|
|
87
|
+
else:
|
|
88
|
+
path = mem.name
|
|
89
|
+
layer.extract(path, path=layer_dir_in_dir)
|
|
90
|
+
|
|
91
|
+
def finalize(self):
|
|
92
|
+
manifest_filename = self._manifest_filename() + '.json'
|
|
93
|
+
manifest_path = os.path.join(self.image_path, manifest_filename)
|
|
94
|
+
with open(manifest_path, 'wb') as f:
|
|
95
|
+
f.write(json.dumps(self.tar_manifest, indent=4,
|
|
96
|
+
sort_keys=True).encode('ascii'))
|
|
97
|
+
|
|
98
|
+
c = {}
|
|
99
|
+
catalog_path = os.path.join(self.image_path, 'catalog.json')
|
|
100
|
+
if os.path.exists(catalog_path):
|
|
101
|
+
with open(catalog_path, 'r') as f:
|
|
102
|
+
c = json.loads(f.read())
|
|
103
|
+
|
|
104
|
+
c.setdefault(self.image, {})
|
|
105
|
+
c[self.image][self.tag] = manifest_filename
|
|
106
|
+
with open(catalog_path, 'w') as f:
|
|
107
|
+
f.write(json.dumps(c, indent=4, sort_keys=True))
|
|
108
|
+
|
|
109
|
+
def write_bundle(self, container_template=constants.RUNC_SPEC_TEMPLATE,
|
|
110
|
+
container_values=None):
|
|
111
|
+
if not container_values:
|
|
112
|
+
container_values = {}
|
|
113
|
+
|
|
114
|
+
rootfs_path = os.path.join(self.image_path, 'rootfs')
|
|
115
|
+
if not os.path.exists(rootfs_path):
|
|
116
|
+
os.makedirs(rootfs_path)
|
|
117
|
+
LOG.info('Writing image bundle to %s' % rootfs_path)
|
|
118
|
+
|
|
119
|
+
working_path = os.path.join(self.image_path, 'working')
|
|
120
|
+
if not os.path.exists(working_path):
|
|
121
|
+
os.makedirs(working_path)
|
|
122
|
+
|
|
123
|
+
delta_path = os.path.join(self.image_path, 'delta')
|
|
124
|
+
if not os.path.exists(delta_path):
|
|
125
|
+
os.makedirs(delta_path)
|
|
126
|
+
|
|
127
|
+
# The newest layer is listed first in the mount command
|
|
128
|
+
layer_dirs = []
|
|
129
|
+
self.tar_manifest[0]['Layers'].reverse()
|
|
130
|
+
for layer in self.tar_manifest[0]['Layers']:
|
|
131
|
+
layer_dirs.append(os.path.join(
|
|
132
|
+
self.image_path, layer.replace('.tar', '')))
|
|
133
|
+
|
|
134
|
+
# Extract the rootfs as overlay mounts
|
|
135
|
+
util.execute('mount -t overlay overlay -o lowerdir=%(layers)s,'
|
|
136
|
+
'upperdir=%(upper)s,workdir=%(working)s %(rootfs)s'
|
|
137
|
+
% {
|
|
138
|
+
'layers': ':'.join(layer_dirs),
|
|
139
|
+
'upper': delta_path,
|
|
140
|
+
'working': working_path,
|
|
141
|
+
'rootfs': rootfs_path
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
# Rename the container configuration to a well known location. This is
|
|
145
|
+
# not part of the OCI specification, but is convenient for now.
|
|
146
|
+
container_config_filename = os.path.join(self.image_path,
|
|
147
|
+
'container-config.json')
|
|
148
|
+
runtime_config_filename = os.path.join(self.image_path, 'config.json')
|
|
149
|
+
os.rename(os.path.join(self.image_path, self.tar_manifest[0]['Config']),
|
|
150
|
+
container_config_filename)
|
|
151
|
+
|
|
152
|
+
common.write_container_config(container_config_filename,
|
|
153
|
+
runtime_config_filename,
|
|
154
|
+
container_template=container_template,
|
|
155
|
+
container_values=container_values)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# OCI bundles are a special case of our directory output -- they don't contain
|
|
2
|
+
# all of the data that a directory output does, and they place the data they
|
|
3
|
+
# do contain into different locations within the directory structure.
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
|
|
9
|
+
from occystrap.constants import RUNC_SPEC_TEMPLATE
|
|
10
|
+
from occystrap import common
|
|
11
|
+
from occystrap.output_directory import DirWriter
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger(__name__)
|
|
15
|
+
LOG.setLevel(logging.INFO)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OCIBundleWriter(DirWriter):
|
|
19
|
+
def __init__(self, image, tag, image_path):
|
|
20
|
+
super(OCIBundleWriter, self).__init__(
|
|
21
|
+
image, tag, image_path, expand=True)
|
|
22
|
+
|
|
23
|
+
def finalize(self):
|
|
24
|
+
self._log_bundle()
|
|
25
|
+
|
|
26
|
+
def write_bundle(self, container_template=RUNC_SPEC_TEMPLATE,
|
|
27
|
+
container_values=None):
|
|
28
|
+
if not container_values:
|
|
29
|
+
container_values = {}
|
|
30
|
+
|
|
31
|
+
rootfs_path = os.path.join(self.image_path, 'rootfs')
|
|
32
|
+
if not os.path.exists(rootfs_path):
|
|
33
|
+
os.makedirs(rootfs_path)
|
|
34
|
+
LOG.info('Writing image bundle to %s' % rootfs_path)
|
|
35
|
+
self._extract_rootfs(rootfs_path)
|
|
36
|
+
|
|
37
|
+
# Remove parts of the output directory which are not present in OCI
|
|
38
|
+
for layer_file in self.tar_manifest[0]['Layers']:
|
|
39
|
+
shutil.rmtree(os.path.join(self.image_path,
|
|
40
|
+
os.path.split(layer_file)[0]))
|
|
41
|
+
|
|
42
|
+
# Rename the container configuration to a well known location. This is
|
|
43
|
+
# not part of the OCI specification, but is convenient for now.
|
|
44
|
+
container_config_filename = os.path.join(self.image_path,
|
|
45
|
+
'container-config.json')
|
|
46
|
+
runtime_config_filename = os.path.join(self.image_path, 'config.json')
|
|
47
|
+
os.rename(os.path.join(self.image_path, self.tar_manifest[0]['Config']),
|
|
48
|
+
container_config_filename)
|
|
49
|
+
|
|
50
|
+
common.write_container_config(container_config_filename,
|
|
51
|
+
runtime_config_filename,
|
|
52
|
+
container_template=container_template,
|
|
53
|
+
container_values=container_values)
|
occystrap/output_tarfile.py
CHANGED
occystrap/util.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
from oslo_concurrency import processutils
|
|
3
4
|
from pbr.version import VersionInfo
|
|
4
5
|
import requests
|
|
5
6
|
|
|
@@ -23,7 +24,7 @@ STATUS_CODES_TO_ERRORS = {
|
|
|
23
24
|
def get_user_agent():
|
|
24
25
|
try:
|
|
25
26
|
version = VersionInfo('occystrap').version_string()
|
|
26
|
-
except:
|
|
27
|
+
except Exception:
|
|
27
28
|
version = '0.0.0'
|
|
28
29
|
return 'Mozilla/5.0 (Ubuntu; Linux x86_64) Occy Strap/%s' % version
|
|
29
30
|
|
|
@@ -74,3 +75,10 @@ def request_url(method, url, headers=None, data=None, stream=False):
|
|
|
74
75
|
raise APIException(
|
|
75
76
|
'API request failed', method, url, r.status_code, r.text, r.headers)
|
|
76
77
|
return r
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def execute(command, check_exit_code=[0], env_variables=None,
|
|
81
|
+
cwd=None):
|
|
82
|
+
return processutils.execute(
|
|
83
|
+
command, check_exit_code=check_exit_code,
|
|
84
|
+
env_variables=env_variables, shell=True, cwd=cwd)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: occystrap
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: occystrap: docker and OCI container tools
|
|
5
5
|
Home-page: https://github.com/shakenfist/occystrap
|
|
6
6
|
Author: Michael Still
|
|
@@ -16,9 +16,11 @@ Classifier: Programming Language :: Python :: 3
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.7
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
Requires-Dist: click (>=7.1.1)
|
|
19
|
+
Requires-Dist: oslo.concurrency
|
|
19
20
|
Requires-Dist: pbr
|
|
20
21
|
Requires-Dist: prettytable
|
|
21
22
|
Requires-Dist: requests
|
|
23
|
+
Requires-Dist: shakenfist-utilities
|
|
22
24
|
|
|
23
25
|
# Occy Strap
|
|
24
26
|
|
|
@@ -98,5 +100,32 @@ occystrap fetch-to-extracted --expand quay.io \
|
|
|
98
100
|
|
|
99
101
|
Note that layers delete files from previous layers with files named ".wh.$previousfilename". These files are _not_ processed in the expanded layers, so that they are visible to the user. They are however processed in the merged layer named for the manifest file.
|
|
100
102
|
|
|
103
|
+
## Generating an OCI runtime bundle
|
|
104
|
+
|
|
105
|
+
This isn't fully supported yet, but you can extract an image to an OCI image bundle
|
|
106
|
+
with the following command:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
occystrap fetch-to-oci registry-1.docker.io library/hello-world latest bar
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
You should then be able to run that container by doing something like:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
cd bar
|
|
116
|
+
sudo apt-get install runc
|
|
117
|
+
sudo runc run id-0001
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Supporting non-default architectures
|
|
121
|
+
|
|
122
|
+
Docker image repositories can store multiple versions of a single image, with each image corresponding to a different (operating system, cpu architecture, cpu variant) tuple. Occy Strap supports letting you specify which to use with global command line flags. Occy Strap defaults to linux amd64 if you don't specify something different. For example, to fetch the linux arm64 v8 image for busybox, you would run:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
occystrap --os linux --architecture arm64 --variant v8 \
|
|
126
|
+
fetch-to-extracted registry-1.docker.io library/busybox \
|
|
127
|
+
latest busybox
|
|
128
|
+
```
|
|
129
|
+
|
|
101
130
|
|
|
102
131
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
occystrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
occystrap/common.py,sha256=Zm4hHpn8RgSXp0W86HhZzpyXq19QIsLJgp9SxK_1QQg,1300
|
|
3
|
+
occystrap/constants.py,sha256=kmOt-12settGbDTW1efpT3UENRQouG9f0ZjgOqWdrIA,4399
|
|
4
|
+
occystrap/docker_extract.py,sha256=j2GSIOShZZh0c5gckXeu-SO7201p4S1EJg3fi7WPQHk,1055
|
|
5
|
+
occystrap/docker_registry.py,sha256=GRpBj1SCxRV_0wOuLIMjMyTRU3Uxinyofzfa2pnwXpo,7936
|
|
6
|
+
occystrap/main.py,sha256=sevQkqAqgnZRrt61DMUPz9xzoE4AIcWYx2_ZurO_Lls,4059
|
|
7
|
+
occystrap/output_directory.py,sha256=S-uL8NSHyHsVKeWsvPRT63E7wE1pWaluGHOFVsS09Xg,11408
|
|
8
|
+
occystrap/output_mounts.py,sha256=AH4vouBF9PmhQ5oGfsSZyN1FhatWbs_20IDlI9psn_k,6381
|
|
9
|
+
occystrap/output_ocibundle.py,sha256=lsVW66ltL2Y-Mxh5Hp2KSCdh9bvsME1142CJ_Uh99oo,2154
|
|
10
|
+
occystrap/output_tarfile.py,sha256=Di8N_2TSo-gQz490D3XOsKCYiv5T_bxZzTRFbd4WGBA,1620
|
|
11
|
+
occystrap/util.py,sha256=nrKfAxDUjb_v9ERDXTmMSSNdJSuLgjdtwOMWk46n0a0,2720
|
|
12
|
+
occystrap/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
occystrap-0.4.0.dist-info/AUTHORS,sha256=toKLUaf9c-NkNow00B_akwMGcGtm-S_ihcC_eql9qWc,34
|
|
14
|
+
occystrap-0.4.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
15
|
+
occystrap-0.4.0.dist-info/METADATA,sha256=mpNFr7zm2FZcmHXnNTZYcsbiA-BqU0mrWQPLKDYYRX4,6308
|
|
16
|
+
occystrap-0.4.0.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
|
|
17
|
+
occystrap-0.4.0.dist-info/entry_points.txt,sha256=51kLRjAxFtC6GWCbTFGezSYMNk5t6xrBmS8Pf7gehiU,50
|
|
18
|
+
occystrap-0.4.0.dist-info/pbr.json,sha256=BKNfUPL0QseayDX9_Ko6PtOTlIdH1276JChGUVbKXk0,46
|
|
19
|
+
occystrap-0.4.0.dist-info/top_level.txt,sha256=06nN7FHq2z_Jpp2PZNm3rGOGUA1cIGlUr6MEZrqgOlc,10
|
|
20
|
+
occystrap-0.4.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"git_version": "c1cb09a", "is_release": true}
|
occystrap-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
occystrap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
occystrap/constants.py,sha256=wZwFTAzrxq8Gb2kT7ZRGa6cAz0nHyvnuzR4KrrY2RI0,56
|
|
3
|
-
occystrap/docker_extract.py,sha256=j2GSIOShZZh0c5gckXeu-SO7201p4S1EJg3fi7WPQHk,1055
|
|
4
|
-
occystrap/docker_registry.py,sha256=CLaUWK9uBHKM3KCGfDWw8myF9nndWdEWIztxGd3Fdeg,5416
|
|
5
|
-
occystrap/main.py,sha256=xNNbPo-cpcUe8hfQgpIyTt0ORsiDtWMF-55ImPb8330,2003
|
|
6
|
-
occystrap/output_directory.py,sha256=vhdX0jQQdzhQoCmsu9MCwl6nl1mon4jvkUXYGUEGmko,9938
|
|
7
|
-
occystrap/output_tarfile.py,sha256=R_kdJd_uitEfl1EyczHZ6n_IvafwzkTVuAIjEbV73Q4,1684
|
|
8
|
-
occystrap/util.py,sha256=Kdzae1pkIRt5uAm1-pgwJFWLI8C7QA7KYkgstPs9yYQ,2440
|
|
9
|
-
occystrap/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
occystrap-0.2.0.dist-info/AUTHORS,sha256=toKLUaf9c-NkNow00B_akwMGcGtm-S_ihcC_eql9qWc,34
|
|
11
|
-
occystrap-0.2.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
-
occystrap-0.2.0.dist-info/METADATA,sha256=3wqUvQETACudMvSBxlgzNWbt1o0_yrFN15CgjT2lotg,5269
|
|
13
|
-
occystrap-0.2.0.dist-info/WHEEL,sha256=g4nMs7d-Xl9-xC9XovUrsDHGXt-FT0E17Yqo92DEfvY,92
|
|
14
|
-
occystrap-0.2.0.dist-info/entry_points.txt,sha256=51kLRjAxFtC6GWCbTFGezSYMNk5t6xrBmS8Pf7gehiU,50
|
|
15
|
-
occystrap-0.2.0.dist-info/pbr.json,sha256=kxHs4Gx_jL71k5Snp4MPDUPUNjY6OSZIcmRk9xnPcPY,46
|
|
16
|
-
occystrap-0.2.0.dist-info/top_level.txt,sha256=06nN7FHq2z_Jpp2PZNm3rGOGUA1cIGlUr6MEZrqgOlc,10
|
|
17
|
-
occystrap-0.2.0.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"git_version": "0f9c4a4", "is_release": true}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|