occystrap 0.1.1__py3-none-any.whl → 0.3.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 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 ADDED
@@ -0,0 +1,186 @@
1
+ CONFIG_FILE = 'config_file'
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
+ }"""
@@ -1,32 +1,43 @@
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
- import gzip
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
+
5
8
  import hashlib
6
9
  import io
7
- import json
8
10
  import logging
9
11
  import os
10
12
  import re
11
13
  import sys
12
- import tarfile
13
14
  import tempfile
14
15
  import zlib
15
16
 
17
+ from occystrap import constants
16
18
  from occystrap import util
17
19
 
18
20
  LOG = logging.getLogger(__name__)
19
21
  LOG.setLevel(logging.INFO)
20
22
 
23
+ DELETED_FILE_RE = re.compile(r'.*/\.wh\.(.*)$')
24
+
21
25
 
22
- DELETED_FILE_RE = re.compile('.*/\.wh\.(.*)$')
26
+ def always_fetch():
27
+ return True
23
28
 
24
29
 
25
30
  class Image(object):
26
- def __init__(self, registry, image, tag):
31
+ def __init__(self, registry, image, tag, os='linux', architecture='amd64', variant='',
32
+ secure=True):
27
33
  self.registry = registry
28
34
  self.image = image
29
35
  self.tag = tag
36
+ self.os = os
37
+ self.architecture = architecture
38
+ self.variant = variant
39
+ self.secure = secure
40
+
30
41
  self._cached_auth = None
31
42
 
32
43
  def request_url(self, method, url, headers=None, data=None, stream=False):
@@ -53,107 +64,129 @@ class Image(object):
53
64
  return util.request_url(
54
65
  method, url, headers=headers, data=data, stream=stream)
55
66
 
56
- def fetch(self, image_path):
67
+ def fetch(self, fetch_callback=always_fetch):
57
68
  LOG.info('Fetching manifest')
69
+ moniker = 'https'
70
+ if not self.secure:
71
+ moniker = 'http'
72
+
58
73
  r = self.request_url(
59
74
  'GET',
60
- 'https://%(registry)s/v2/%(image)s/manifests/%(tag)s'
75
+ '%(moniker)s://%(registry)s/v2/%(image)s/manifests/%(tag)s'
61
76
  % {
77
+ 'moniker': moniker,
62
78
  'registry': self.registry,
63
79
  'image': self.image,
64
80
  'tag': self.tag
65
81
  },
66
- headers={'Accept': 'application/vnd.docker.distribution.manifest.v2+json'})
67
- manifest = r.json()
68
- LOG.info('Manifest says: %s' % manifest)
82
+ headers={'Accept': ('application/vnd.docker.distribution.manifest.v2+json,'
83
+ 'application/vnd.docker.distribution.manifest.list.v2+json')})
84
+
85
+ config_digest = None
86
+ if r.headers['Content-Type'] == 'application/vnd.docker.distribution.manifest.v2+json':
87
+ manifest = r.json()
88
+ config_digest = manifest['config']['digest']
89
+ elif r.headers['Content-Type'] == 'application/vnd.docker.distribution.manifest.list.v2+json':
90
+ for m in r.json()['manifests']:
91
+ if 'variant' in m['platform']:
92
+ LOG.info('Found manifest for %s on %s %s'
93
+ % (m['platform']['os'], m['platform']['architecture'],
94
+ m['platform']['variant']))
95
+ else:
96
+ LOG.info('Found manifest for %s on %s'
97
+ % (m['platform']['os'], m['platform']['architecture']))
98
+
99
+ if (m['platform']['os'] == self.os and
100
+ m['platform']['architecture'] == self.architecture and
101
+ m['platform'].get('variant', '') == self.variant):
102
+ LOG.info('Fetching matching manifest')
103
+ r = self.request_url(
104
+ 'GET',
105
+ '%(moniker)s://%(registry)s/v2/%(image)s/manifests/%(tag)s'
106
+ % {
107
+ 'moniker': moniker,
108
+ 'registry': self.registry,
109
+ 'image': self.image,
110
+ 'tag': m['digest']
111
+ },
112
+ headers={'Accept': ('application/vnd.docker.distribution.manifest.v2+json')})
113
+ manifest = r.json()
114
+ config_digest = manifest['config']['digest']
115
+
116
+ if not config_digest:
117
+ raise Exception('Could not find a matching manifest for this '
118
+ 'os / architecture / variant')
119
+ else:
120
+ raise Exception('Unknown manifest content type %s!' %
121
+ r.headers['Content-Type'])
69
122
 
70
123
  LOG.info('Fetching config file')
71
124
  r = self.request_url(
72
125
  'GET',
73
- 'https://%(registry)s/v2/%(image)s/blobs/%(config)s'
126
+ '%(moniker)s://%(registry)s/v2/%(image)s/blobs/%(config)s'
74
127
  % {
128
+ 'moniker': moniker,
75
129
  'registry': self.registry,
76
130
  'image': self.image,
77
- 'config': manifest['config']['digest']
131
+ 'config': config_digest
78
132
  })
79
133
  config = r.content
80
134
  h = hashlib.sha256()
81
135
  h.update(config)
82
- if h.hexdigest() != manifest['config']['digest'].split(':')[1]:
136
+ if h.hexdigest() != config_digest.split(':')[1]:
83
137
  LOG.error('Hash verification failed for image config blob (%s vs %s)'
84
- % (manifest['config']['digest'].split(':')[1], h.hexdigest()))
138
+ % (config_digest.split(':')[1], h.hexdigest()))
85
139
  sys.exit(1)
86
140
 
87
- tar_manifest = [{
88
- 'Layers': [],
89
- 'RepoTags': ['%s:%s' % (self.image.split('/')[-1], self.tag)]
90
- }]
91
-
92
- with tarfile.open(image_path, 'w') as image_tar:
93
- LOG.info('Writing config file to tarball')
94
- config_filename = ('%s.json'
95
- % manifest['config']['digest'].split(':')[1])
96
- ti = tarfile.TarInfo(config_filename)
97
- ti.size = len(config)
98
- image_tar.addfile(ti, io.BytesIO(config))
99
- tar_manifest[0]['Config'] = config_filename
100
-
101
- LOG.info('There are %d image layers' % len(manifest['layers']))
102
- for layer in manifest['layers']:
103
- LOG.info('Fetching layer %s (%d bytes)'
104
- % (layer['digest'], layer['size']))
105
- r = self.request_url(
106
- 'GET',
107
- 'https://%(registry)s/v2/%(image)s/blobs/%(layer)s'
108
- % {
109
- 'registry': self.registry,
110
- 'image': self.image,
111
- 'layer': layer['digest']
112
- },
113
- stream=True)
114
-
115
- LOG.info('Writing layer to tarball')
116
- layer_filename = layer['digest'].split(':')[1]
117
-
118
- # We can use zlib for streaming decompression, but we need to tell it
119
- # to ignore the gzip header which it doesn't understand. Unfortunately
120
- # tarfile doesn't do streaming writes (and we need to know the
121
- # decompressed size before we can write to the tarfile), so we stream
122
- # to a temporary file on disk.
123
- try:
124
- h = hashlib.sha256()
125
- d = zlib.decompressobj(16 + zlib.MAX_WBITS)
126
-
127
- with tempfile.NamedTemporaryFile(delete=False) as tf:
128
- LOG.info('Temporary file for layer is %s' % tf.name)
129
- for chunk in r.iter_content(8192):
130
- tf.write(d.decompress(chunk))
131
- h.update(chunk)
132
-
133
- if h.hexdigest() != layer_filename:
134
- LOG.error('Hash verification failed for layer (%s vs %s)'
135
- % (layer_filename, h.hexdigest()))
136
- sys.exit(1)
137
-
138
- layer_filename += '/layer.tar'
139
- image_tar.add(
140
- tf.name, arcname=layer_filename)
141
- tar_manifest[0]['Layers'].append(layer_filename)
142
-
143
- with tarfile.open(tf.name) as layer:
144
- for mem in layer.getmembers():
145
- m = DELETED_FILE_RE.match(mem.name)
146
- if m:
147
- LOG.info('Layer tarball contains deleted file: %s'
148
- % mem.name)
149
-
150
- finally:
151
- os.unlink(tf.name)
152
-
153
- LOG.info('Writing manifest file to tarball')
154
- encoded_manifest = json.dumps(tar_manifest).encode('utf-8')
155
- ti = tarfile.TarInfo('manifest.json')
156
- ti.size = len(encoded_manifest)
157
- image_tar.addfile(ti, io.BytesIO(encoded_manifest))
141
+ config_filename = ('%s.json' % config_digest.split(':')[1])
142
+ yield (constants.CONFIG_FILE, config_filename,
143
+ io.BytesIO(config))
144
+
145
+ LOG.info('There are %d image layers' % len(manifest['layers']))
146
+ for layer in manifest['layers']:
147
+ layer_filename = layer['digest'].split(':')[1]
148
+ if not fetch_callback(layer_filename):
149
+ LOG.info('Fetch callback says skip layer %s' % layer['digest'])
150
+ yield (constants.IMAGE_LAYER, layer_filename, None)
151
+ continue
152
+
153
+ LOG.info('Fetching layer %s (%d bytes)'
154
+ % (layer['digest'], layer['size']))
155
+ r = self.request_url(
156
+ 'GET',
157
+ '%(moniker)s://%(registry)s/v2/%(image)s/blobs/%(layer)s'
158
+ % {
159
+ 'moniker': moniker,
160
+ 'registry': self.registry,
161
+ 'image': self.image,
162
+ 'layer': layer['digest']
163
+ },
164
+ stream=True)
165
+
166
+ # We can use zlib for streaming decompression, but we need to tell it
167
+ # to ignore the gzip header which it doesn't understand. Unfortunately
168
+ # tarfile doesn't do streaming writes (and we need to know the
169
+ # decompressed size before we can write to the tarfile), so we stream
170
+ # to a temporary file on disk.
171
+ try:
172
+ h = hashlib.sha256()
173
+ d = zlib.decompressobj(16 + zlib.MAX_WBITS)
174
+
175
+ with tempfile.NamedTemporaryFile(delete=False) as tf:
176
+ LOG.info('Temporary file for layer is %s' % tf.name)
177
+ for chunk in r.iter_content(8192):
178
+ tf.write(d.decompress(chunk))
179
+ h.update(chunk)
180
+
181
+ if h.hexdigest() != layer_filename:
182
+ LOG.error('Hash verification failed for layer (%s vs %s)'
183
+ % (layer_filename, h.hexdigest()))
184
+ sys.exit(1)
185
+
186
+ with open(tf.name, 'rb') as f:
187
+ yield (constants.IMAGE_LAYER, layer_filename, f)
188
+
189
+ finally:
190
+ os.unlink(tf.name)
158
191
 
159
192
  LOG.info('Done')
occystrap/main.py CHANGED
@@ -1,32 +1,137 @@
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
8
+ from occystrap import output_directory
9
+ from occystrap import output_mounts
10
+ from occystrap import output_ocibundle
11
+ from occystrap import output_tarfile
5
12
 
6
- logging.basicConfig(level=logging.INFO)
7
13
 
8
- LOG = logging.getLogger(__name__)
9
- LOG.setLevel(logging.INFO)
14
+ LOG = logs.setup_console(__name__)
10
15
 
11
16
 
12
17
  @click.group()
13
- @click.option('--verbose/--no-verbose', default=False)
18
+ @click.option('--verbose', is_flag=True)
19
+ @click.option('--os', default='linux')
20
+ @click.option('--architecture', default='amd64')
21
+ @click.option('--variant', default='')
14
22
  @click.pass_context
15
- def cli(ctx, verbose=None):
23
+ def cli(ctx, verbose=None, os=None, architecture=None, variant=None):
16
24
  if verbose:
17
25
  logging.basicConfig(level=logging.DEBUG)
18
26
  LOG.setLevel(logging.DEBUG)
19
27
 
28
+ if not ctx.obj:
29
+ ctx.obj = {}
30
+ ctx.obj['OS'] = os
31
+ ctx.obj['ARCHITECTURE'] = architecture
32
+ ctx.obj['VARIANT'] = variant
33
+
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)
38
+ for image_element in img.fetch(fetch_callback=output.fetch_callback):
39
+ output.process_image_element(*image_element)
40
+ output.finalize()
41
+
20
42
 
21
43
  @click.command()
22
44
  @click.argument('registry')
23
45
  @click.argument('image')
24
46
  @click.argument('tag')
47
+ @click.argument('path')
48
+ @click.option('--use-unique-names', is_flag=True)
49
+ @click.option('--expand', is_flag=True)
50
+ @click.option('--insecure', is_flag=True, default=False)
51
+ @click.pass_context
52
+ def fetch_to_extracted(ctx, registry, image, tag, path, use_unique_names,
53
+ expand, insecure):
54
+ d = output_directory.DirWriter(
55
+ image, tag, path, unique_names=use_unique_names, expand=expand)
56
+ _fetch(registry, image, tag, d, ctx.obj['OS'], ctx.obj['ARCHITECTURE'],
57
+ ctx.obj['VARIANT'], secure=(not insecure))
58
+
59
+ if expand:
60
+ d.write_bundle()
61
+
62
+
63
+ cli.add_command(fetch_to_extracted)
64
+
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
+
83
+ @click.command()
84
+ @click.argument('registry')
85
+ @click.argument('image')
86
+ @click.argument('tag')
87
+ @click.argument('tarfile')
88
+ @click.option('--insecure', is_flag=True, default=False)
89
+ @click.pass_context
90
+ def fetch_to_tarfile(ctx, registry, image, tag, tarfile, insecure):
91
+ tar = output_tarfile.TarWriter(image, tag, tarfile)
92
+ _fetch(registry, image, tag, tar, ctx.obj['OS'], ctx.obj['ARCHITECTURE'],
93
+ ctx.obj['VARIANT'], secure=(not insecure))
94
+
95
+
96
+ cli.add_command(fetch_to_tarfile)
97
+
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
+
123
+ @click.command()
124
+ @click.argument('path')
125
+ @click.argument('image')
126
+ @click.argument('tag')
25
127
  @click.argument('tarfile')
26
128
  @click.pass_context
27
- def fetch(ctx, registry, image, tag, tarfile):
28
- img = docker_registry.Image(registry, image, tag)
29
- img.fetch(tarfile)
129
+ def recreate_image(ctx, path, image, tag, tarfile):
130
+ d = output_directory.DirReader(path, image, tag)
131
+ tar = output_tarfile.TarWriter(image, tag, tarfile)
132
+ for image_element in d.fetch():
133
+ tar.process_image_element(*image_element)
134
+ tar.finalize()
30
135
 
31
136
 
32
- cli.add_command(fetch)
137
+ cli.add_command(recreate_image)