Skip to content

Commit 3e224e4

Browse files
committed
Docker rebase
Allows to rebase docker image on another parent. v2 support only (no means to test on v1).
1 parent 89e0297 commit 3e224e4

File tree

12 files changed

+225
-82
lines changed

12 files changed

+225
-82
lines changed

.travis.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ language: python
22
python:
33
- "2.7"
44
- "3.4"
5+
- "3.5"
6+
- "3.6"
57
- "pypy"
8+
- "pypy3"
69
install: "pip install -r requirements.txt"
710
script:
811
- "py.test -v"

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ test-py34: prepare
1212
test-py35: prepare
1313
tox -e py35 -- tests
1414

15+
test-py36: prepare
16+
tox -e py36 -- tests
17+
18+
test-py37: prepare
19+
tox -e py37 -- tests
20+
1521
test-unit: prepare
1622
tox -- tests/test_unit*
1723

@@ -28,6 +34,11 @@ else
2834
@sudo chmod +x /usr/bin/docker
2935
endif
3036

37+
ci-install-pythons:
38+
curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash
39+
pyenv update
40+
for pyver in 2.7.14 3.4.8 3.5.5 3.6.5; do pyenv install -s $$pyver; done
41+
3142
ci-publish-junit:
3243
@mkdir -p ${CIRCLE_TEST_REPORTS}
3344
@cp target/junit*.xml ${CIRCLE_TEST_REPORTS}

circle.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ machine:
1212
CI: true
1313

1414
dependencies:
15+
pre:
16+
- mkdir -p ~/.pyenv
17+
- make -f Makefile ci-install-pythons
18+
cache_directories:
19+
- ~/.pyenv
1520
override:
1621
- pip install tox tox-pyenv docker-py>=1.7.2 six
17-
- pyenv local 2.7.11 3.4.4 3.5.1
22+
- pyenv local 2.7.14 3.4.8 3.5.5 3.6.5
1823
post:
1924
- docker version
2025
- docker info
@@ -23,5 +28,9 @@ test:
2328
override:
2429
- case $CIRCLE_NODE_INDEX in 0) make test-py27 ;; 1) make test-py34 ;; 2) make test-py35 ;; esac:
2530
parallel: true
31+
- make test-py36:
32+
parallel: true
33+
- make test-py36:
34+
parallel: true
2635
post:
2736
- make ci-publish-junit

docker_squash/cli.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,22 @@ def run(self):
6060
'--version', action='version', help='Show version and exit', version=version)
6161

6262
parser.add_argument('image', help='Image to be squashed')
63-
parser.add_argument(
64-
'-d', '--development', action='store_true', help='Does not clean up after failure for easier debugging')
65-
parser.add_argument(
66-
'-f', '--from-layer', help='ID of the layer or image ID or image name. If not specified will squash all layers in the image')
67-
parser.add_argument(
68-
'-t', '--tag', help="Specify the tag to be used for the new image. If not specified no tag will be applied")
69-
parser.add_argument(
70-
'-c', '--cleanup', action='store_true', help="Remove source image from Docker after squashing")
71-
parser.add_argument(
72-
'--tmp-dir', help='Temporary directory to be created and used')
73-
parser.add_argument(
74-
'--output-path', help='Path where the image should be stored after squashing. If not provided, image will be loaded into Docker daemon')
63+
parser.add_argument('-r', '--rebase',
64+
help='Rebase the image on a different "FROM"')
65+
parser.add_argument('-d', '--development', action='store_true',
66+
help='Does not clean up after failure for easier debugging')
67+
parser.add_argument('-f', '--from-layer',
68+
help='ID of the layer or image ID or image name. '
69+
'If not specified will squash all layers in the image')
70+
parser.add_argument('-t', '--tag',
71+
help="Specify the tag to be used for the new image. If not specified no tag will be applied")
72+
parser.add_argument('-c', '--cleanup', action='store_true',
73+
help="Remove source image from Docker after squashing")
74+
parser.add_argument('--tmp-dir',
75+
help='Temporary directory to be created and used')
76+
parser.add_argument('--output-path',
77+
help='Path where the image should be stored after squashing. '
78+
'If not provided, image will be loaded into Docker daemon')
7579

7680
args = parser.parse_args()
7781

@@ -84,7 +88,8 @@ def run(self):
8488

8589
try:
8690
squash.Squash(log=self.log, image=args.image,
87-
from_layer=args.from_layer, tag=args.tag, output_path=args.output_path, tmp_dir=args.tmp_dir, development=args.development, cleanup=args.cleanup).run()
91+
from_layer=args.from_layer, tag=args.tag, output_path=args.output_path, tmp_dir=args.tmp_dir,
92+
development=args.development, cleanup=args.cleanup, rebase=args.rebase).run()
8893
except KeyboardInterrupt:
8994
self.log.error("Program interrupted by user, exiting...")
9095
sys.exit(1)
@@ -96,8 +101,9 @@ def run(self):
96101
else:
97102
self.log.error(str(e))
98103

99-
self.log.error(
100-
"Execution failed, consult logs above. If you think this is our fault, please file an issue: https://github.com/goldmann/docker-squash/issues, thanks!")
104+
self.log.error("Execution failed, consult logs above. "
105+
"If you think this is our fault, please file an issue: "
106+
"https://github.com/goldmann/docker-squash/issues, thanks!")
101107

102108
if isinstance(e, SquashError):
103109
sys.exit(e.code)

docker_squash/image.py

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,19 @@ class Image(object):
4444
FORMAT = None
4545
""" Image format version """
4646

47-
def __init__(self, log, docker, image, from_layer, tmp_dir=None, tag=None):
47+
def __init__(self, log, docker, image, from_layer, tmp_dir=None, tag=None, rebase=None):
4848
self.log = log
4949
self.debug = self.log.isEnabledFor(logging.DEBUG)
5050
self.docker = docker
5151
self.image = image
5252
self.from_layer = from_layer
5353
self.tag = tag
54+
self.rebase = rebase
5455
self.image_name = None
5556
self.image_tag = None
5657
self.squash_id = None
5758

59+
5860
# Workaround for https://play.golang.org/p/sCsWMXYxqy
5961
#
6062
# Golang doesn't add padding to microseconds when marshaling
@@ -71,6 +73,7 @@ def __init__(self, log, docker, image, from_layer, tmp_dir=None, tag=None):
7173

7274
def squash(self):
7375
self._before_squashing()
76+
self.log.info("Squashing image '%s'..." % self.image)
7477
ret = self._squash()
7578
self._after_squashing()
7679

@@ -94,12 +97,14 @@ def _initialize_directories(self):
9497

9598
# Temporary location on the disk of the old, unpacked *image*
9699
self.old_image_dir = os.path.join(self.tmp_dir, "old")
100+
# Temporary location on the disk of the rebase, unpacked *image*
101+
self.rebase_image_dir = os.path.join(self.tmp_dir, "rebase")
97102
# Temporary location on the disk of the new, unpacked, squashed *image*
98103
self.new_image_dir = os.path.join(self.tmp_dir, "new")
99104
# Temporary location on the disk of the squashed *layer*
100105
self.squashed_dir = os.path.join(self.new_image_dir, "squashed")
101106

102-
for d in self.old_image_dir, self.new_image_dir:
107+
for d in self.old_image_dir, self.new_image_dir, self.rebase_image_dir:
103108
os.makedirs(d)
104109

105110
def _squash_id(self, layer):
@@ -152,8 +157,16 @@ def _before_squashing(self):
152157
try:
153158
self.old_image_id = self.docker.inspect_image(self.image)['Id']
154159
except SquashError:
155-
raise SquashError(
156-
"Could not get the image ID to squash, please check provided 'image' argument: %s" % self.image)
160+
raise SquashError("Could not get the image ID to squash, "
161+
"please check provided 'image' argument: %s" % self.image)
162+
163+
if self.rebase:
164+
# The image id or name of the image to rebase to
165+
try:
166+
self.rebase = self.docker.inspect_image(self.rebase)['Id']
167+
except SquashError:
168+
raise SquashError("Could not get the image ID to rebase to, "
169+
"please check provided 'rebase' argument: %s" % self.rebase)
157170

158171
self.old_image_layers = []
159172

@@ -166,32 +179,36 @@ def _before_squashing(self):
166179
self.log.debug("Old layers: %s", self.old_image_layers)
167180

168181
# By default - squash all layers.
169-
if self.from_layer == None:
182+
if self.from_layer is None:
170183
self.from_layer = len(self.old_image_layers)
171184

172185
try:
173186
number_of_layers = int(self.from_layer)
174187

175-
self.log.debug(
176-
"We detected number of layers as the argument to squash")
188+
self.log.debug("We detected number of layers as the argument to squash")
177189
except ValueError:
178190
self.log.debug("We detected layer as the argument to squash")
179191

180192
squash_id = self._squash_id(self.from_layer)
181193

182194
if not squash_id:
183-
raise SquashError(
184-
"The %s layer could not be found in the %s image" % (self.from_layer, self.image))
195+
raise SquashError("The %s layer could not be found in the %s image" % (self.from_layer, self.image))
185196

186-
number_of_layers = len(self.old_image_layers) - \
187-
self.old_image_layers.index(squash_id) - 1
197+
number_of_layers = len(self.old_image_layers) - self.old_image_layers.index(squash_id) - 1
188198

189199
self._validate_number_of_layers(number_of_layers)
190200

191201
marker = len(self.old_image_layers) - number_of_layers
192202

193203
self.layers_to_squash = self.old_image_layers[marker:]
194-
self.layers_to_move = self.old_image_layers[:marker]
204+
if self.rebase:
205+
self.layers_to_move = []
206+
self._read_layers(self.layers_to_move, self.rebase)
207+
self.layers_to_move.reverse()
208+
else:
209+
self.layers_to_move = self.old_image_layers[:marker]
210+
211+
self.old_image_squash_marker = marker
195212

196213
self.log.info("Checking if squashing is necessary...")
197214

@@ -201,18 +218,18 @@ def _before_squashing(self):
201218
if len(self.layers_to_squash) == 1:
202219
raise SquashUnnecessaryError("Single layer marked to squash, no squashing is required")
203220

204-
self.log.info("Attempting to squash last %s layers...",
205-
number_of_layers)
221+
self.log.info("Attempting to squash last %s layers%s...", number_of_layers,
222+
" rebasing on %s" % self.rebase if self.rebase else "")
206223
self.log.debug("Layers to squash: %s", self.layers_to_squash)
207224
self.log.debug("Layers to move: %s", self.layers_to_move)
208225

209226
# Fetch the image and unpack it on the fly to the old image directory
210227
self._save_image(self.old_image_id, self.old_image_dir)
228+
if self.rebase:
229+
self._save_image(self.rebase, self.rebase_image_dir)
211230

212231
self.size_before = self._dir_size(self.old_image_dir)
213232

214-
self.log.info("Squashing image '%s'..." % self.image)
215-
216233
def _after_squashing(self):
217234
self.log.debug("Removing from disk already squashed layers...")
218235
shutil.rmtree(self.old_image_dir, ignore_errors=True)
@@ -698,7 +715,7 @@ def _squash_layers(self, layers_to_squash, layers_to_move):
698715

699716
# Find all files in layers that we don't squash
700717
files_in_layers_to_move = self._files_in_layers(
701-
layers_to_move, self.old_image_dir)
718+
layers_to_move, self.old_image_dir if not self.rebase else self.rebase_image_dir)
702719

703720
with tarfile.open(self.squashed_tar, 'w', format=tarfile.PAX_FORMAT) as squashed_tar:
704721
to_skip = []

docker_squash/squash.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
class Squash(object):
1515

1616
def __init__(self, log, image, docker=None, from_layer=None, tag=None, tmp_dir=None,
17-
output_path=None, load_image=True, development=False, cleanup=False):
17+
output_path=None, load_image=True, development=False, cleanup=False, rebase=None):
1818
self.log = log
1919
self.docker = docker
2020
self.image = image
@@ -25,6 +25,7 @@ def __init__(self, log, image, docker=None, from_layer=None, tag=None, tmp_dir=N
2525
self.load_image = load_image
2626
self.development = development
2727
self.cleanup = cleanup
28+
self.rebase = rebase
2829

2930
if not docker:
3031
self.docker = common.docker_client(self.log)
@@ -48,10 +49,10 @@ def run(self):
4849

4950
if StrictVersion(docker_version['ApiVersion']) >= StrictVersion("1.22"):
5051
image = V2Image(self.log, self.docker, self.image,
51-
self.from_layer, self.tmp_dir, self.tag)
52+
self.from_layer, self.tmp_dir, self.tag, self.rebase)
5253
else:
5354
image = V1Image(self.log, self.docker, self.image,
54-
self.from_layer, self.tmp_dir, self.tag)
55+
self.from_layer, self.tmp_dir, self.tag, self.rebase)
5556

5657
self.log.info("Using %s image format" % image.FORMAT)
5758

docker_squash/v1_image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def _squash(self):
3333
self._write_version_file(self.squashed_dir)
3434
# Move all the layers that should be untouched
3535
self._move_layers(self.layers_to_move,
36-
self.old_image_dir, self.new_image_dir)
36+
self.old_image_dir if not self.rebase else self.rebase_image_dir, self.new_image_dir)
3737

3838
config_file = os.path.join(
3939
self.old_image_dir, self.old_image_id, "json")

0 commit comments

Comments
 (0)