Skip to content

Commit 9c547ab

Browse files
committed
Feature: Could not save docker images locally
Solution: Create a new subcommand, "aleph container create"
1 parent 5c2256c commit 9c547ab

File tree

7 files changed

+186
-69
lines changed

7 files changed

+186
-69
lines changed

src/aleph_client/commands/container/cli_command.py

+64-36
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from base64 import b32encode, b16decode
1010
from typing import Optional, Dict, List
1111

12-
from aleph_message.models import StoreMessage
12+
from aleph_message.models import (
13+
StoreMessage,
14+
ProgramMessage
15+
)
1316

1417
from aleph_client import synchronous
1518
from aleph_client.account import _load_account, AccountFromPrivateKey
@@ -30,7 +33,10 @@
3033
yes_no_input
3134
)
3235

33-
from .save import save_tar
36+
37+
38+
from aleph_client.commands.container.save import save_tar
39+
from aleph_client.commands.container.utils import create_container_volume
3440

3541
logger = logging.getLogger(__name__)
3642
app = typer.Typer()
@@ -52,7 +58,7 @@ def upload_file(
5258
else StorageEnum.storage
5359
)
5460
logger.debug("Uploading file")
55-
result = synchronous.create_store(
61+
result: StoreMessage = synchronous.create_store(
5662
account=account,
5763
file_content=file_content,
5864
storage_engine=storage_engine,
@@ -65,19 +71,38 @@ def upload_file(
6571
typer.echo(f"{json.dumps(result, indent=4)}")
6672
return result
6773

74+
def MutuallyExclusiveBoolean():
75+
marker = None
76+
def callback(ctx: typer.Context, param: typer.CallbackParam, value: str):
77+
# Add cli option to group if it was called with a value
78+
nonlocal marker
79+
if value is False:
80+
return value
81+
if marker is None:
82+
marker = param.name
83+
if param.name != marker:
84+
raise typer.BadParameter(
85+
f"{param.name} is mutually exclusive with {marker}")
86+
return value
87+
return callback
88+
89+
exclusivity_callback = MutuallyExclusiveBoolean()
90+
6891
@app.command()
6992
def upload(
7093
image: str = typer.Argument(..., help="Path to an image archive exported with docker save."),
7194
path: str = typer.Argument(..., metavar="SCRIPT", help="A small script to start your container with parameters"),
72-
from_remote: bool = typer.Option(False, "--from-remote", "-r", help=" If --from-remote, IMAGE is a registry to pull the image from. e.g: library/alpine, library/ubuntu:latest"),
73-
from_daemon: bool = typer.Option(False, "--from-daemon", "-d", help=" If --from-daemon, IMAGE is an image in local docker deamon storage. You need docker installed for this command"),
95+
from_remote: bool = typer.Option(False, "--from-remote", help=" If --from-remote, IMAGE is a registry to pull the image from. e.g: library/alpine, library/ubuntu:latest", callback=exclusivity_callback),
96+
from_daemon: bool = typer.Option(False, "--from-daemon", help=" If --from-daemon, IMAGE is an image in local docker deamon storage. You need docker installed for this command", callback=exclusivity_callback),
97+
from_created: bool = typer.Option(False, "--from-created", help=" If --from-created, IMAGE the path to a file created with 'aleph container create'", callback=exclusivity_callback),
7498
channel: str = typer.Option(settings.DEFAULT_CHANNEL, help=help_strings.CHANNEL),
7599
memory: int = typer.Option(settings.DEFAULT_VM_MEMORY, help="Maximum memory allocation on vm in MiB"),
76100
vcpus: int = typer.Option(settings.DEFAULT_VM_VCPUS, help="Number of virtual cpus to allocate."),
77101
timeout_seconds: float = typer.Option(settings.DEFAULT_VM_TIMEOUT, help="If vm is not called after [timeout_seconds] it will shutdown"),
78102
private_key: Optional[str] = typer.Option(settings.PRIVATE_KEY_STRING, help=help_strings.PRIVATE_KEY),
79103
private_key_file: Optional[Path] = typer.Option(settings.PRIVATE_KEY_FILE, help=help_strings.PRIVATE_KEY_FILE),
80104
docker_mountpoint: Optional[Path] = typer.Option(settings.DEFAULT_DOCKER_VOLUME_MOUNTPOINT, "--docker-mountpoint", help="The path where the created docker image volume will be mounted"),
105+
optimize: bool = typer.Option(True, help="Activate volume size optimization"),
81106
print_messages: bool = typer.Option(False),
82107
print_code_message: bool = typer.Option(False),
83108
print_program_message: bool = typer.Option(False),
@@ -87,31 +112,11 @@ def upload(
87112
Deploy a docker container on Aleph virtual machines.
88113
Unless otherwise specified, you don't need docker on your machine to run this command.
89114
"""
90-
if from_remote or from_daemon:
91-
raise NotImplementedError()
92-
# echo(f"Downloading {image}")
93-
# registry = Registry()
94-
# tag = "latest"
95-
# if ":" in image:
96-
# l = image.split(":")
97-
# tag = l[-1]
98-
# image = l[0]
99-
# print(tag)
100-
# image_object = registry.pull_image(image, tag)
101-
# manifest = registry.get_manifest_configuration(image, tag)
102-
# image_archive = os.path.abspath(f"{str(uuid4())}.tar")
103-
# image_object.write_filename(image_archive)
104-
# image = image_archive
105-
# print(manifest)
106115
typer.echo("Preparing image for vm runtime")
107-
docker_data_path = os.path.abspath("docker-data")
108-
save_tar(image, docker_data_path, settings=settings.DOCKER_SETTINGS)
109-
if not settings.CODE_USES_SQUASHFS:
110-
typer.echo("The command mksquashfs must be installed!")
111-
typer.Exit(2)
112-
logger.debug("Creating squashfs archive...")
113-
os.system(f"mksquashfs {docker_data_path} {docker_data_path}.squashfs -noappend")
114-
docker_data_path = f"{docker_data_path}.squashfs"
116+
docker_data_path=image
117+
if not from_created:
118+
docker_data_path = os.path.abspath("docker-data.squashfs")
119+
create_container_volume(image, docker_data_path, from_remote, from_daemon, optimize, settings)
115120
assert os.path.isfile(docker_data_path)
116121
encoding = Encoding.squashfs
117122
path = os.path.abspath(path)
@@ -140,16 +145,19 @@ def upload(
140145
docker_upload_result: StoreMessage = upload_file(docker_data_path, account, channel, print_messages, print_code_message)
141146
volumes.append({
142147
"comment": "Docker container volume",
143-
"mount": docker_mountpoint,
144-
"ref": docker_upload_result["item_hash"],
148+
"mount": str(docker_mountpoint),
149+
"ref": str(docker_upload_result.item_hash),
145150
"use_latest": True,
146151
})
152+
153+
typer.echo(f"Docker image upload message address: {docker_upload_result.item_hash}")
154+
147155
program_result: StoreMessage = upload_file(path, account, channel, print_messages, print_code_message)
148156

149157
# Register the program
150-
result = synchronous.create_program(
158+
result: ProgramMessage = synchronous.create_program(
151159
account=account,
152-
program_ref=program_result["item_hash"],
160+
program_ref=program_result.item_hash,
153161
entrypoint=entrypoint,
154162
runtime=settings.DEFAULT_DOCKER_RUNTIME_ID,
155163
storage_engine=StorageEnum.storage,
@@ -161,25 +169,45 @@ def upload(
161169
volumes=volumes,
162170
subscriptions=subscriptions,
163171
environment_variables={
164-
"DOCKER_MOUNTPOINT": docker_mountpoint
172+
"DOCKER_MOUNTPOINT": str(docker_mountpoint)
165173
}
166174
)
167175
logger.debug("Upload finished")
168176
if print_messages or print_program_message:
169177
typer.echo(f"{json.dumps(result, indent=4)}")
170178

171-
hash: str = result["item_hash"]
179+
hash: str = result.item_hash
172180
hash_base32 = b32encode(b16decode(hash.upper())).strip(b"=").lower().decode()
173181

182+
174183
typer.echo(
175184
f"Your program has been uploaded on Aleph .\n\n"
176185
"Available on:\n"
177186
f" {settings.VM_URL_PATH.format(hash=hash)}\n"
178187
f" {settings.VM_URL_HOST.format(hash_base32=hash_base32)}\n"
179188
"Visualise on:\n https://explorer.aleph.im/address/"
180-
f"{result['chain']}/{result['sender']}/message/PROGRAM/{hash}\n"
189+
f"{result.chain}/{result.sender}/message/PROGRAM/{hash}\n"
181190
)
182191

183192
finally:
184193
# Prevent aiohttp unclosed connector warning
185194
asyncio.get_event_loop().run_until_complete(get_fallback_session().close())
195+
196+
@app.command()
197+
def create(
198+
image: str = typer.Argument(..., help="Path to an image archive exported with docker save."),
199+
output: str = typer.Argument(..., help="The path where you want "),
200+
from_remote: bool = typer.Option(False, "--from-remote", help=" If --from-remote, IMAGE is a registry to pull the image from. e.g: library/alpine, library/ubuntu:latest", callback=exclusivity_callback),
201+
from_daemon: bool = typer.Option(False, "--from-daemon", help=" If --from-daemon, IMAGE is an image in local docker deamon storage. You need docker installed for this command", callback=exclusivity_callback),
202+
optimize: bool = typer.Option(True, help="Activate volume size optimization"),
203+
):
204+
"""
205+
Use a docker image to create an Aleph compatible image on your local machine.
206+
You can later upload it with 'aleph container upload --from-'
207+
"""
208+
try:
209+
create_container_volume(image, output, from_remote, from_daemon, optimize, settings)
210+
typer.echo(f"Container volume created at {output}")
211+
except Exception as e:
212+
print(e)
213+
return
+8-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import sys
2-
from .image import Image
3-
from .storage_drivers import create_storage_driver
2+
from aleph_client.commands.container.image import Image
3+
from aleph_client.commands.container.storage_drivers import create_storage_driver
44
import os
55
from shutil import rmtree
6-
from .docker_conf import docker_settings, DockerSettings
6+
from aleph_client.commands.container.docker_conf import docker_settings, DockerSettings
77

88
dirs = {
99
"vfs": 0o710,
@@ -12,27 +12,27 @@
1212
"swarm": 0o700,
1313
"runtimes": 0o700,
1414
"network": 0o750,
15-
"containers": 0o710,
1615
"trust": 0o700,
1716
"volumes": 0o701,
1817
"buildkit": 0o711,
1918
"containers": 0o710,
2019
"tmp": 0o700,
21-
"containerd": 0o711,
2220
}
2321

22+
2423
def populate_dir(output_path: str):
2524
print("populating")
2625
path = os.path.abspath(output_path)
2726
if os.path.exists(output_path) and os.path.isdir(output_path):
2827
try:
2928
rmtree(output_path)
3029
except:
31-
raise "" #TODO: handle error
30+
raise "" # TODO: handle error
3231
os.makedirs(output_path, 0o710)
3332
for d, mode in dirs.items():
3433
os.makedirs(os.path.join(path, d), mode)
3534

35+
3636
def save_tar(archive_path: str, output_path: str, settings: DockerSettings):
3737
archive_path = os.path.abspath(archive_path)
3838
output_path = os.path.abspath(output_path)
@@ -42,5 +42,6 @@ def save_tar(archive_path: str, output_path: str, settings: DockerSettings):
4242
driver = create_storage_driver(image, output_path, settings)
4343
driver.create_file_architecture()
4444

45+
4546
if __name__ == "__main__":
46-
save_tar(sys.argv[1], sys.argv[2], docker_settings)
47+
save_tar(sys.argv[1], sys.argv[2], docker_settings)

src/aleph_client/commands/container/storage_drivers.py

+40-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import tarfile
2-
from typing import Dict
2+
from typing import Dict, List
33
from .image import Image
44
import os
55
import json
@@ -11,6 +11,7 @@
1111
import gzip
1212
from .docker_conf import DockerSettings, StorageDriverEnum
1313

14+
1415
class IStorageDriver:
1516
def create_file_architecture(self):
1617
"""
@@ -70,6 +71,12 @@ def create_distribution(self, output_dir: str):
7071
"""
7172
raise NotImplementedError(f"You must implement this method")
7273

74+
def optimize(self, output_dir: str):
75+
"""
76+
Reproduce /var/lib/docker/image/{storage_driver}/disctibution
77+
in output_dir based on an image object.
78+
"""
79+
raise NotImplementedError(f"You must implement this method")
7380

7481

7582
# Since aleph vms can be running with an unknown host configuration,
@@ -104,13 +111,16 @@ def create_imagedb(self, output_dir: str):
104111
os.makedirs(os.path.join(output_dir, "imagedb"), 0o700)
105112
os.makedirs(os.path.join(output_dir, "imagedb", "content"), 0o700)
106113
os.makedirs(os.path.join(output_dir, "imagedb", "metadata"), 0o700)
107-
os.makedirs(os.path.join(output_dir, "imagedb", "content", "sha256"), 0o700)
108-
os.makedirs(os.path.join(output_dir, "imagedb", "metadata", "sha256"), 0o700)
114+
os.makedirs(os.path.join(output_dir, "imagedb",
115+
"content", "sha256"), 0o700)
116+
os.makedirs(os.path.join(output_dir, "imagedb",
117+
"metadata", "sha256"), 0o700)
109118
# os.makedirs(os.path.join(metadata, self.image.image_digest))
110119
content = os.path.join(output_dir, "imagedb", "content", "sha256")
111120
path = os.path.join(content, self.image.image_digest)
112121
with open(path, "w") as f:
113-
f.write(json.dumps(self.image.config, separators=(',', ':'))) # This file must be dumped compactly in order to keep the correct sha256 digest
122+
# This file must be dumped compactly in order to keep the correct sha256 digest
123+
f.write(json.dumps(self.image.config, separators=(',', ':')))
114124
os.chmod(path, 0o0600)
115125
# with open(os.path.join(metadata, self.image.image_digest, "parent"), "w") as f:
116126
# f.write(self.image.config['config']['Image'])
@@ -148,10 +158,10 @@ def save_layer_metadata(path: str, diff: str, cacheid: str, size: int, previous_
148158
fd.write(previous_chain_id)
149159
os.chmod(dest, 0o600)
150160

151-
152161
def copy_layer(src: str, dest: str) -> None:
153162
for folder in os.listdir(src):
154-
subprocess.check_output(["cp", "-r", os.path.join(src, folder), dest])
163+
subprocess.check_output(
164+
["cp", "-r", os.path.join(src, folder), dest])
155165

156166
def compute_layer_size(tar_data_json_path: str) -> int:
157167
size = 0
@@ -160,12 +170,15 @@ def compute_layer_size(tar_data_json_path: str) -> int:
160170
"["
161171
+ archive.read().decode().replace("}\n{", "},\n{")
162172
+ "]"
163-
) # fixes poor formatting from tar-split
173+
) # fixes poor formatting from tar-split
164174
for elem in data:
165175
if "size" in elem.keys():
166-
size =+ elem["size"]
176+
size = + elem["size"]
167177
return size
168178

179+
def remove_unused_layers(layers_dir: str, keep: List[str]):
180+
return
181+
169182
def extract_layer(path: str, archive_path: str, layerdb_subdir: str) -> int:
170183
cwd = os.getcwd()
171184
tmp_dir = tempfile.mkdtemp()
@@ -183,42 +196,47 @@ def extract_layer(path: str, archive_path: str, layerdb_subdir: str) -> int:
183196
# Mandatory if one plans to export a docker image to a tar file
184197
# https://github.com/vbatts/tar-split
185198
if self.use_tarsplit:
186-
tar_data_json = os.path.join(layerdb_subdir, "tar-split.json.gz")
187-
os.system(f"tar-split disasm --output {tar_data_json} {tar_src} | tar -C . -x")
188-
size = compute_layer_size(tar_data_json) # Differs from expected. Only messes with docker image size listing
199+
tar_data_json = os.path.join(
200+
layerdb_subdir, "tar-split.json.gz")
201+
os.system(
202+
f"tar-split disasm --output {tar_data_json} {tar_src} | tar -C . -x")
203+
# Differs from expected. Only messes with docker image size listing
204+
size = compute_layer_size(tar_data_json)
189205
os.remove(tar_src)
190206

191207
# Also works, but won't be able to export images
192208
else:
193209
with tarfile.open(tar_src, "r") as tar:
194210
os.remove(tar_src)
195211
tar.extractall()
196-
size=0
212+
size = 0
197213
os.rmdir(tmp_dir)
198214
os.chdir(cwd)
199215
return size
200216

201217
previous_cache_id = None
202218
for i in range(0, len(self.image.chain_ids)):
203219
chain_id = self.image.chain_ids[i]
204-
layerdb_subdir = os.path.join(layerdb_path, chain_id.replace("sha256:", ""))
220+
layerdb_subdir = os.path.join(
221+
layerdb_path, chain_id.replace("sha256:", ""))
205222
os.makedirs(layerdb_subdir, 0o700)
206223
cache_id = (str(uuid4()) + str(uuid4())).replace("-", "")
207224

208225
layer_id = self.image.layers_ids[i]
209226
current_layer_path = os.path.join(layers_dir, cache_id)
210227
os.makedirs(current_layer_path, 0o700)
211228

212-
213229
# Merge layers
214230
# The last layer contains changes from all the previous ones
215231
if previous_cache_id:
216-
previous_layer_path = os.path.join(layers_dir, previous_cache_id)
232+
previous_layer_path = os.path.join(
233+
layers_dir, previous_cache_id)
217234
copy_layer(previous_layer_path, current_layer_path)
218235
if (self.optimize):
219236
rmtree(previous_layer_path)
220237
previous_cache_id = cache_id
221-
size = extract_layer(current_layer_path, self.image.archive_path, layerdb_subdir)
238+
size = extract_layer(current_layer_path,
239+
self.image.archive_path, layerdb_subdir)
222240
save_layer_metadata(
223241
path=layerdb_subdir,
224242
diff=self.image.diff_ids[i],
@@ -228,6 +246,11 @@ def extract_layer(path: str, archive_path: str, layerdb_subdir: str) -> int:
228246
if i > 0
229247
else None
230248
)
249+
if self.optimize:
250+
layer_to_keep = os.path.join(
251+
layers_dir, previous_cache_id
252+
)
253+
remove_unused_layers(layers_dir, layer_to_keep)
231254

232255

233256
def create_storage_driver(
@@ -237,4 +260,4 @@ def create_storage_driver(
237260
) -> IStorageDriver:
238261
if settings.storage_driver.kind == StorageDriverEnum.VFS:
239262
return Vfs(image, output_dir, settings)
240-
raise NotImplementedError("Only vfs supported now")
263+
raise NotImplementedError("Only vfs supported now")
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
FROM alpine:latest
22

33
WORKDIR /app
4+
RUN touch file.txt
5+
RUN mkdir folder.d
46

57
CMD ["/bin/sh"]

src/aleph_client/commands/container/test_data/custom-dockerd

100755100644
File mode changed.

0 commit comments

Comments
 (0)