1
+ import json
1
2
import re
2
- from typing import Any , Dict , List , Optional
3
+ from pathlib import Path
4
+ from typing import Any , Dict , List , Optional , Union
3
5
4
6
import click
5
7
6
8
from pulpcore .cli .common .context import (
7
9
EntityFieldDefinition ,
10
+ PulpContext ,
8
11
PulpEntityContext ,
9
12
PulpRemoteContext ,
10
13
PulpRepositoryContext ,
14
+ pass_pulp_context ,
11
15
pass_repository_context ,
12
16
)
13
17
from pulpcore .cli .common .generic import (
17
21
label_command ,
18
22
label_select_option ,
19
23
list_command ,
24
+ load_json_callback ,
20
25
name_option ,
21
26
pulp_group ,
22
27
repository_content_command ,
41
46
PulpContainerRepositoryContext ,
42
47
PulpContainerTagContext ,
43
48
)
49
+ from pulpcore .cli .core .context import PulpArtifactContext
44
50
from pulpcore .cli .core .generic import task_command
45
51
46
52
translation = get_translation (__name__ )
@@ -57,6 +63,22 @@ def _tag_callback(ctx: click.Context, param: click.Parameter, value: str) -> str
57
63
return value
58
64
59
65
66
+ def _directory_or_json_callback (
67
+ ctx : click .Context , param : click .Parameter , value : Optional [str ]
68
+ ) -> Union [str , Path , None ]:
69
+ if not value :
70
+ return value
71
+ uvalue : Union [str , Path ]
72
+ try :
73
+ uvalue = load_json_callback (ctx , param , value )
74
+ except click .ClickException :
75
+ uvalue = Path (value )
76
+ if not uvalue .exists () or not uvalue .is_dir ():
77
+ raise click .ClickException (_ ("{} is not a valid directory" ).format (value ))
78
+
79
+ return uvalue
80
+
81
+
60
82
source_option = resource_option (
61
83
"--source" ,
62
84
default_plugin = "container" ,
@@ -114,6 +136,7 @@ def repository() -> None:
114
136
"blob" : PulpContainerBlobContext ,
115
137
}
116
138
container_context = (PulpContainerRepositoryContext ,)
139
+ push_container_context = (PulpContainerPushRepositoryContext ,)
117
140
118
141
repository .add_command (list_command (decorators = [label_select_option ]))
119
142
repository .add_command (show_command (decorators = lookup_options ))
@@ -286,3 +309,93 @@ def copy_manifest(
286
309
digests = digests or None ,
287
310
media_types = media_types or None ,
288
311
)
312
+
313
+
314
+ def upload_file (pulp_ctx : PulpContext , file_location : str ) -> str :
315
+ try :
316
+ with click .open_file (file_location , "r" ) as fp :
317
+ artifact_ctx = PulpArtifactContext (pulp_ctx )
318
+ click .echo (_ ("Uploading {} as artifact" ).format (file_location ))
319
+ artifact_href = artifact_ctx .upload (fp )
320
+ except OSError :
321
+ raise click .ClickException (
322
+ _ ("Failed to load content from {file}" ).format (file = file_location )
323
+ )
324
+ click .echo (_ ("Uploaded file: {}" ).format (artifact_href ))
325
+ return artifact_href # type: ignore
326
+
327
+
328
+ @repository .command (allowed_with_contexts = container_context )
329
+ @name_option
330
+ @href_option
331
+ @click .option (
332
+ "--containerfile" ,
333
+ help = _ (
334
+ "An artifact href of an uploaded Containerfile. Can also be a local Containerfile to be"
335
+ " uploaded using @."
336
+ ),
337
+ required = True ,
338
+ )
339
+ @click .option ("--tag" , help = _ ("A tag name for the new image being built." ))
340
+ @click .option (
341
+ "--artifacts" ,
342
+ help = _ (
343
+ "Directory of files to be uploaded and used during the build. Or a JSON string where each"
344
+ " key is an artifact href and the value is it's relative path (name) inside the "
345
+ "/pulp_working_directory of the build container executing the Containerfile."
346
+ ),
347
+ callback = _directory_or_json_callback ,
348
+ )
349
+ @pass_repository_context
350
+ @pass_pulp_context
351
+ def build_image (
352
+ pulp_ctx : PulpContext ,
353
+ repository_ctx : PulpContainerRepositoryContext ,
354
+ containerfile : str ,
355
+ tag : Optional [str ],
356
+ artifacts : Union [str , Path , None ],
357
+ ) -> None :
358
+ if not repository_ctx .capable ("build" ):
359
+ raise click .ClickException (_ ("Repository does not support image building." ))
360
+
361
+ container_artifact_href : str
362
+ artifacts_json : Optional [str ] = None
363
+ # Upload necessary files as artifacts if specified
364
+ if containerfile [0 ] == "@" :
365
+ container_artifact_href = upload_file (pulp_ctx , containerfile [1 :])
366
+ else :
367
+ artifact_ctx = PulpArtifactContext (pulp_ctx , pulp_href = containerfile )
368
+ container_artifact_href = artifact_ctx .pulp_href
369
+
370
+ if artifacts :
371
+ if isinstance (artifacts , Path ):
372
+ # Upload files in directory
373
+ artifact_hrefs = {}
374
+ for child in artifacts .iterdir ():
375
+ # Can the directory structure be non-flat?
376
+ if child .is_file ():
377
+ artifact_href = upload_file (pulp_ctx , str (child ))
378
+ artifact_hrefs [artifact_href ] = child .name
379
+ artifacts_json = json .dumps (artifact_hrefs )
380
+ else :
381
+ artifacts_json = artifacts
382
+
383
+ repository_ctx .build_image (container_artifact_href , tag , artifacts_json )
384
+
385
+
386
+ @repository .command (allowed_with_contexts = push_container_context )
387
+ @name_option
388
+ @href_option
389
+ @click .option ("--digest" , help = _ ("SHA256 digest of the Manifest file" ), required = True )
390
+ @pass_repository_context
391
+ def remove_image (
392
+ repository_ctx : PulpContainerPushRepositoryContext ,
393
+ digest : str ,
394
+ ) -> None :
395
+ digest = digest .strip ()
396
+ if not digest .startswith ("sha256:" ):
397
+ digest = f"sha256:{ digest } "
398
+ if len (digest ) != 71 : # len("sha256:") + 64
399
+ raise click .ClickException ("Improper SHA256, please provide a valid 64 digit digest." )
400
+
401
+ repository_ctx .remove_image (digest )
0 commit comments