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.
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
@@ -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