docker-squash 1.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.
- docker_squash/__init__.py +1 -0
- docker_squash/cli.py +163 -0
- docker_squash/errors.py +10 -0
- docker_squash/image.py +1130 -0
- docker_squash/lib/__init__.py +0 -0
- docker_squash/lib/common.py +81 -0
- docker_squash/squash.py +170 -0
- docker_squash/v1_image.py +87 -0
- docker_squash/v2_image.py +425 -0
- docker_squash/version.py +1 -0
- docker_squash-1.3.0.dist-info/METADATA +242 -0
- docker_squash-1.3.0.dist-info/RECORD +16 -0
- docker_squash-1.3.0.dist-info/WHEEL +5 -0
- docker_squash-1.3.0.dist-info/entry_points.txt +2 -0
- docker_squash-1.3.0.dist-info/licenses/LICENSE +21 -0
- docker_squash-1.3.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
import docker
|
|
7
|
+
import docker.errors
|
|
8
|
+
import docker.utils
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from docker_squash.errors import Error
|
|
12
|
+
|
|
13
|
+
# First try to import Docker client using the API
|
|
14
|
+
# available in version 2 of the library and fall
|
|
15
|
+
# back to version 1
|
|
16
|
+
try:
|
|
17
|
+
from docker.api.client import APIClient as APIClientClass
|
|
18
|
+
except ImportError:
|
|
19
|
+
from docker.client import Client as APIClientClass
|
|
20
|
+
|
|
21
|
+
DEFAULT_TIMEOUT_SECONDS = 600
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def docker_client(log):
|
|
25
|
+
log.debug("Preparing Docker client...")
|
|
26
|
+
|
|
27
|
+
# Default timeout 10 minutes
|
|
28
|
+
try:
|
|
29
|
+
timeout = int(os.getenv("DOCKER_TIMEOUT", 600))
|
|
30
|
+
except ValueError:
|
|
31
|
+
raise Error(
|
|
32
|
+
"Provided timeout value: %s cannot be parsed as integer, exiting."
|
|
33
|
+
% os.getenv("DOCKER_TIMEOUT")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if not timeout > 0:
|
|
37
|
+
raise Error(
|
|
38
|
+
"Provided timeout value needs to be greater than zero, currently: %s, exiting."
|
|
39
|
+
% timeout
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# backwards compat
|
|
43
|
+
try:
|
|
44
|
+
os.environ["DOCKER_HOST"] = os.environ["DOCKER_CONNECTION"]
|
|
45
|
+
log.warn("DOCKER_CONNECTION is deprecated, please use DOCKER_HOST instead")
|
|
46
|
+
except KeyError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
params: Dict[str, Any] = {"version": "auto"}
|
|
50
|
+
params.update(docker.utils.kwargs_from_env())
|
|
51
|
+
params["timeout"] = timeout
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
client = APIClientClass(**params)
|
|
55
|
+
except docker.errors.DockerException as e:
|
|
56
|
+
log.error(
|
|
57
|
+
"Could not create Docker client, please make sure that you specified valid parameters in the 'DOCKER_HOST' environment variable."
|
|
58
|
+
)
|
|
59
|
+
raise Error("Error while creating the Docker client: %s" % e)
|
|
60
|
+
|
|
61
|
+
if client and valid_docker_connection(client):
|
|
62
|
+
log.debug("Docker client ready")
|
|
63
|
+
return client
|
|
64
|
+
else:
|
|
65
|
+
log.error(
|
|
66
|
+
"Could not connect to the Docker daemon, please make sure the Docker daemon is running."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if os.environ.get("DOCKER_HOST"):
|
|
70
|
+
log.error(
|
|
71
|
+
"If Docker daemon is running, please make sure that you specified valid parameters in the 'DOCKER_HOST' environment variable."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
raise Error("Cannot connect to Docker daemon")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def valid_docker_connection(client):
|
|
78
|
+
try:
|
|
79
|
+
return client.ping()
|
|
80
|
+
except requests.exceptions.ConnectionError:
|
|
81
|
+
return False
|
docker_squash/squash.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import docker.errors as docker_errors
|
|
8
|
+
from docker.api.client import APIClient
|
|
9
|
+
from packaging import version as packaging_version
|
|
10
|
+
|
|
11
|
+
from docker_squash.errors import SquashError
|
|
12
|
+
from docker_squash.image import Image
|
|
13
|
+
from docker_squash.lib import common
|
|
14
|
+
from docker_squash.v1_image import V1Image
|
|
15
|
+
from docker_squash.v2_image import V2Image
|
|
16
|
+
from docker_squash.version import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Squash(object):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
log,
|
|
23
|
+
image,
|
|
24
|
+
docker: Optional[APIClient] = None,
|
|
25
|
+
from_layer: Optional[str] = None,
|
|
26
|
+
tag: Optional[str] = None,
|
|
27
|
+
comment: Optional[str] = "",
|
|
28
|
+
tmp_dir: Optional[str] = None,
|
|
29
|
+
output_path: Optional[str] = None,
|
|
30
|
+
load_image: Optional[bool] = True,
|
|
31
|
+
cleanup: Optional[bool] = False,
|
|
32
|
+
):
|
|
33
|
+
self.log: Logger = log
|
|
34
|
+
self.docker: APIClient = docker if docker else common.docker_client(self.log)
|
|
35
|
+
self.image: str = image
|
|
36
|
+
self.from_layer: Optional[str] = from_layer
|
|
37
|
+
self.tag: Optional[str] = tag
|
|
38
|
+
self.comment: Optional[str] = comment
|
|
39
|
+
self.tmp_dir: Optional[str] = tmp_dir
|
|
40
|
+
self.output_path: Optional[str] = output_path
|
|
41
|
+
self.load_image: Optional[bool] = load_image
|
|
42
|
+
self.cleanup: Optional[bool] = cleanup
|
|
43
|
+
self.development = False
|
|
44
|
+
|
|
45
|
+
if tag == image and cleanup:
|
|
46
|
+
log.warning("Tag is the same as image; preventing cleanup")
|
|
47
|
+
self.cleanup = False
|
|
48
|
+
if tmp_dir:
|
|
49
|
+
self.development = True
|
|
50
|
+
|
|
51
|
+
def run(self):
|
|
52
|
+
docker_version = self.docker.version()
|
|
53
|
+
self.log.info(
|
|
54
|
+
"docker-squash version %s, Docker %s, API %s..."
|
|
55
|
+
% (__version__, docker_version["Version"], docker_version["ApiVersion"])
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if self.image is None:
|
|
59
|
+
raise SquashError("Image is not provided")
|
|
60
|
+
|
|
61
|
+
if not (self.output_path or self.load_image):
|
|
62
|
+
self.log.warning(
|
|
63
|
+
"No output path specified and loading into Docker is not selected either; squashed image would not accessible, proceeding with squashing doesn't make sense"
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if self.output_path and os.path.exists(self.output_path):
|
|
68
|
+
self.log.warning(
|
|
69
|
+
"Path '%s' specified as output path where the squashed image should be saved already exists, it'll be overriden"
|
|
70
|
+
% self.output_path
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
image: Image
|
|
74
|
+
|
|
75
|
+
if packaging_version.parse(
|
|
76
|
+
docker_version["ApiVersion"]
|
|
77
|
+
) >= packaging_version.parse("1.22"):
|
|
78
|
+
image = V2Image(
|
|
79
|
+
self.log,
|
|
80
|
+
self.docker,
|
|
81
|
+
self.image,
|
|
82
|
+
self.from_layer,
|
|
83
|
+
self.tmp_dir,
|
|
84
|
+
self.tag,
|
|
85
|
+
self.comment,
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
image = V1Image(
|
|
89
|
+
self.log,
|
|
90
|
+
self.docker,
|
|
91
|
+
self.image,
|
|
92
|
+
self.from_layer,
|
|
93
|
+
self.tmp_dir,
|
|
94
|
+
self.tag,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
self.log.info("Using %s image format" % image.FORMAT)
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
return self.squash(image)
|
|
101
|
+
except Exception:
|
|
102
|
+
# https://github.com/goldmann/docker-scripts/issues/44
|
|
103
|
+
# If development mode is not enabled, make sure we clean up the
|
|
104
|
+
# temporary directory
|
|
105
|
+
if not self.development:
|
|
106
|
+
image.cleanup()
|
|
107
|
+
|
|
108
|
+
raise
|
|
109
|
+
|
|
110
|
+
def _cleanup(self):
|
|
111
|
+
try:
|
|
112
|
+
image_id = self.docker.inspect_image(self.image)["Id"]
|
|
113
|
+
except docker_errors.APIError as ex:
|
|
114
|
+
self.log.warning(
|
|
115
|
+
"Could not get the image ID for {} image: {}, skipping cleanup after squashing".format(
|
|
116
|
+
self.image, str(ex)
|
|
117
|
+
)
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
self.log.info("Removing old {} image...".format(self.image))
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
self.docker.remove_image(image_id, force=False, noprune=False)
|
|
125
|
+
self.log.info("Image {} removed!".format(self.image))
|
|
126
|
+
except docker_errors.APIError as ex:
|
|
127
|
+
self.log.warning(
|
|
128
|
+
"Could not remove image {}: {}, skipping cleanup after squashing".format(
|
|
129
|
+
self.image, str(ex)
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def squash(self, image: Image):
|
|
134
|
+
# Do the actual squashing
|
|
135
|
+
new_image_id = image.squash()
|
|
136
|
+
|
|
137
|
+
self.log.info("New squashed image ID is %s" % new_image_id)
|
|
138
|
+
|
|
139
|
+
if self.output_path:
|
|
140
|
+
# Move the tar archive to the specified path
|
|
141
|
+
image.export_tar_archive(self.output_path)
|
|
142
|
+
|
|
143
|
+
if self.load_image:
|
|
144
|
+
# Load squashed image into Docker; use the ID assigned by the
|
|
145
|
+
# daemon, which differs from the computed one on some image stores
|
|
146
|
+
loaded_image_id = image.load_squashed_image()
|
|
147
|
+
|
|
148
|
+
if loaded_image_id:
|
|
149
|
+
if loaded_image_id != new_image_id:
|
|
150
|
+
self.log.info(
|
|
151
|
+
"Image ID assigned by the Docker daemon is %s" % loaded_image_id
|
|
152
|
+
)
|
|
153
|
+
new_image_id = loaded_image_id
|
|
154
|
+
|
|
155
|
+
# If development mode is not enabled, make sure we clean up the
|
|
156
|
+
# temporary directory
|
|
157
|
+
if not self.development:
|
|
158
|
+
# Clean up all temporary files
|
|
159
|
+
image.cleanup()
|
|
160
|
+
|
|
161
|
+
# Remove the source image - this is the only possible way
|
|
162
|
+
# to remove orphaned layers from Docker daemon at the build time.
|
|
163
|
+
# We cannot use here a tag name because it could be used as the target,
|
|
164
|
+
# squashed image tag - we need to use the image ID.
|
|
165
|
+
if self.cleanup:
|
|
166
|
+
self._cleanup()
|
|
167
|
+
|
|
168
|
+
self.log.info("Done")
|
|
169
|
+
|
|
170
|
+
return new_image_id
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import os
|
|
3
|
+
import random
|
|
4
|
+
import shutil
|
|
5
|
+
|
|
6
|
+
from docker_squash.image import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class V1Image(Image):
|
|
10
|
+
FORMAT = "v1"
|
|
11
|
+
|
|
12
|
+
def _generate_image_id(self):
|
|
13
|
+
while True:
|
|
14
|
+
image_id = hashlib.sha256(
|
|
15
|
+
str(random.getrandbits(128)).encode("utf8")
|
|
16
|
+
).hexdigest()
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
int(image_id[0:10])
|
|
20
|
+
except ValueError:
|
|
21
|
+
# All good!
|
|
22
|
+
return image_id
|
|
23
|
+
|
|
24
|
+
def _before_squashing(self):
|
|
25
|
+
super(V1Image, self)._before_squashing()
|
|
26
|
+
|
|
27
|
+
if self.layers_to_move:
|
|
28
|
+
self.squash_id = self.layers_to_move[-1]
|
|
29
|
+
|
|
30
|
+
def _squash(self) -> str:
|
|
31
|
+
# Prepare the directory
|
|
32
|
+
os.makedirs(self.squashed_dir)
|
|
33
|
+
self._squash_layers(self.layers_to_squash, self.layers_to_move)
|
|
34
|
+
self._write_version_file(self.squashed_dir)
|
|
35
|
+
# Move all the layers that should be untouched
|
|
36
|
+
self._move_layers(self.layers_to_move, self.old_image_dir, self.new_image_dir)
|
|
37
|
+
|
|
38
|
+
config_file = os.path.join(self.old_image_dir, self.old_image_id, "json")
|
|
39
|
+
|
|
40
|
+
image_id = self._update_squashed_layer_metadata(config_file, self.squashed_dir)
|
|
41
|
+
shutil.move(self.squashed_dir, os.path.join(self.new_image_dir, image_id))
|
|
42
|
+
repositories_file = os.path.join(self.new_image_dir, "repositories")
|
|
43
|
+
self._generate_repositories_json(
|
|
44
|
+
repositories_file, image_id, self.image_name, self.image_tag
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return image_id
|
|
48
|
+
|
|
49
|
+
def _update_squashed_layer_metadata(self, old_json_file, squashed_dir):
|
|
50
|
+
image_id = self._generate_image_id()
|
|
51
|
+
|
|
52
|
+
metadata = self._read_old_metadata(old_json_file)
|
|
53
|
+
|
|
54
|
+
# Modify common metadata fields
|
|
55
|
+
if self.squash_id:
|
|
56
|
+
metadata["config"]["Image"] = self.squash_id
|
|
57
|
+
else:
|
|
58
|
+
metadata["config"].pop("Image", None)
|
|
59
|
+
|
|
60
|
+
if "parent_id" in metadata and self.squash_id:
|
|
61
|
+
metadata["parent_id"] = "sha256:%s" % self.squash_id
|
|
62
|
+
else:
|
|
63
|
+
metadata.pop("parent_id", None)
|
|
64
|
+
|
|
65
|
+
metadata.pop("layer_id", None)
|
|
66
|
+
|
|
67
|
+
metadata["created"] = self.date
|
|
68
|
+
|
|
69
|
+
# Remove unnecessary fields
|
|
70
|
+
del metadata["container_config"]
|
|
71
|
+
del metadata["container"]
|
|
72
|
+
del metadata["config"]["Hostname"]
|
|
73
|
+
|
|
74
|
+
if self.squash_id:
|
|
75
|
+
metadata["parent"] = self.squash_id
|
|
76
|
+
else:
|
|
77
|
+
metadata.pop("parent", None)
|
|
78
|
+
|
|
79
|
+
metadata["id"] = image_id
|
|
80
|
+
metadata["Size"] = os.path.getsize(os.path.join(squashed_dir, "layer.tar"))
|
|
81
|
+
json_metadata = self._dump_json(metadata)[0]
|
|
82
|
+
|
|
83
|
+
self._write_json_metadata(
|
|
84
|
+
"%s" % json_metadata, os.path.join(squashed_dir, "json")
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return image_id
|