Skip to content

Commit 824361c

Browse files
committed
Docker rebase
Allows to rebase docker image on another parent. v2 support only (no means to test on v1).
1 parent 903050a commit 824361c

File tree

12 files changed

+223
-82
lines changed

12 files changed

+223
-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: 8 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
@@ -27,5 +32,7 @@ test:
2732
parallel: true
2833
- make test-py35:
2934
parallel: true
35+
- make test-py36:
36+
parallel: true
3037
post:
3138
- 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
@@ -42,17 +42,19 @@ class Image(object):
4242
FORMAT = None
4343
""" Image format version """
4444

45-
def __init__(self, log, docker, image, from_layer, tmp_dir=None, tag=None):
45+
def __init__(self, log, docker, image, from_layer, tmp_dir=None, tag=None, rebase=None):
4646
self.log = log
4747
self.debug = self.log.isEnabledFor(logging.DEBUG)
4848
self.docker = docker
4949
self.image = image
5050
self.from_layer = from_layer
5151
self.tag = tag
52+
self.rebase = rebase
5253
self.image_name = None
5354
self.image_tag = None
5455
self.squash_id = None
5556

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

7072
def squash(self):
7173
self._before_squashing()
74+
self.log.info("Squashing image '%s'..." % self.image)
7275
ret = self._squash()
7376
self._after_squashing()
7477

@@ -92,12 +95,14 @@ def _initialize_directories(self):
9295

9396
# Temporary location on the disk of the old, unpacked *image*
9497
self.old_image_dir = os.path.join(self.tmp_dir, "old")
98+
# Temporary location on the disk of the rebase, unpacked *image*
99+
self.rebase_image_dir = os.path.join(self.tmp_dir, "rebase")
95100
# Temporary location on the disk of the new, unpacked, squashed *image*
96101
self.new_image_dir = os.path.join(self.tmp_dir, "new")
97102
# Temporary location on the disk of the squashed *layer*
98103
self.squashed_dir = os.path.join(self.new_image_dir, "squashed")
99104

100-
for d in self.old_image_dir, self.new_image_dir:
105+
for d in self.old_image_dir, self.new_image_dir, self.rebase_image_dir:
101106
os.makedirs(d)
102107

103108
def _squash_id(self, layer):
@@ -150,8 +155,16 @@ def _before_squashing(self):
150155
try:
151156
self.old_image_id = self.docker.inspect_image(self.image)['Id']
152157
except SquashError:
153-
raise SquashError(
154-
"Could not get the image ID to squash, please check provided 'image' argument: %s" % self.image)
158+
raise SquashError("Could not get the image ID to squash, "
159+
"please check provided 'image' argument: %s" % self.image)
160+
161+
if self.rebase:
162+
# The image id or name of the image to rebase to
163+
try:
164+
self.rebase = self.docker.inspect_image(self.rebase)['Id']
165+
except SquashError:
166+
raise SquashError("Could not get the image ID to rebase to, "
167+
"please check provided 'rebase' argument: %s" % self.rebase)
155168

156169
self.old_image_layers = []
157170

@@ -164,32 +177,36 @@ def _before_squashing(self):
164177
self.log.debug("Old layers: %s", self.old_image_layers)
165178

166179
# By default - squash all layers.
167-
if self.from_layer == None:
180+
if self.from_layer is None:
168181
self.from_layer = len(self.old_image_layers)
169182

170183
try:
171184
number_of_layers = int(self.from_layer)
172185

173-
self.log.debug(
174-
"We detected number of layers as the argument to squash")
186+
self.log.debug("We detected number of layers as the argument to squash")
175187
except ValueError:
176188
self.log.debug("We detected layer as the argument to squash")
177189

178190
squash_id = self._squash_id(self.from_layer)
179191

180192
if not squash_id:
181-
raise SquashError(
182-
"The %s layer could not be found in the %s image" % (self.from_layer, self.image))
193+
raise SquashError("The %s layer could not be found in the %s image" % (self.from_layer, self.image))
183194

184-
number_of_layers = len(self.old_image_layers) - \
185-
self.old_image_layers.index(squash_id) - 1
195+
number_of_layers = len(self.old_image_layers) - self.old_image_layers.index(squash_id) - 1
186196

187197
self._validate_number_of_layers(number_of_layers)
188198

189199
marker = len(self.old_image_layers) - number_of_layers
190200

191201
self.layers_to_squash = self.old_image_layers[marker:]
192-
self.layers_to_move = self.old_image_layers[:marker]
202+
if self.rebase:
203+
self.layers_to_move = []
204+
self._read_layers(self.layers_to_move, self.rebase)
205+
self.layers_to_move.reverse()
206+
else:
207+
self.layers_to_move = self.old_image_layers[:marker]
208+
209+
self.old_image_squash_marker = marker
193210

194211
self.log.info("Checking if squashing is necessary...")
195212

@@ -199,18 +216,18 @@ def _before_squashing(self):
199216
if len(self.layers_to_squash) == 1:
200217
raise SquashUnnecessaryError("Single layer marked to squash, no squashing is required")
201218

202-
self.log.info("Attempting to squash last %s layers...",
203-
number_of_layers)
219+
self.log.info("Attempting to squash last %s layers%s...", number_of_layers,
220+
" rebasing on %s" % self.rebase if self.rebase else "")
204221
self.log.debug("Layers to squash: %s", self.layers_to_squash)
205222
self.log.debug("Layers to move: %s", self.layers_to_move)
206223

207224
# Fetch the image and unpack it on the fly to the old image directory
208225
self._save_image(self.old_image_id, self.old_image_dir)
226+
if self.rebase:
227+
self._save_image(self.rebase, self.rebase_image_dir)
209228

210229
self.size_before = self._dir_size(self.old_image_dir)
211230

212-
self.log.info("Squashing image '%s'..." % self.image)
213-
214231
def _after_squashing(self):
215232
self.log.debug("Removing from disk already squashed layers...")
216233
shutil.rmtree(self.old_image_dir, ignore_errors=True)
@@ -670,7 +687,7 @@ def _squash_layers(self, layers_to_squash, layers_to_move):
670687

671688
# Find all files in layers that we don't squash
672689
files_in_layers_to_move = self._files_in_layers(
673-
layers_to_move, self.old_image_dir)
690+
layers_to_move, self.old_image_dir if not self.rebase else self.rebase_image_dir)
674691

675692
with tarfile.open(self.squashed_tar, 'w', format=tarfile.PAX_FORMAT) as squashed_tar:
676693
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)