Skip to content

Commit ef4f733

Browse files
committed
Add support for pulp-python attestations feature
Assisted By: Cursor Composer
1 parent 293aef9 commit ef4f733

File tree

8 files changed

+136
-52
lines changed

8 files changed

+136
-52
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added support for uploading attestations with Python Package content.
2+
Added support for uploading Python Provenance content.
3+
Added support for specifying syncing of Python Provenance content.

pulp-glue/pulp_glue/python/context.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ class PulpPythonContentContext(PulpContentContext):
4040
CAPABILITIES = {"upload": []}
4141

4242

43+
class PulpPythonProvenanceContext(PulpContentContext):
44+
PLUGIN = "python"
45+
RESOURCE_TYPE = "provenance"
46+
ENTITY = _("python provenance")
47+
ENTITIES = _("python provenances")
48+
HREF = "python_python_provenance_content_href"
49+
ID_PREFIX = "content_python_provenance"
50+
NEEDS_PLUGINS = [PluginRequirement("python", specifier=">=3.22.0")]
51+
52+
4353
class PulpPythonDistributionContext(PulpDistributionContext):
4454
PLUGIN = "python"
4555
RESOURCE_TYPE = "python"

pulpcore/cli/python/content.py

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
from pulp_glue.common.context import PluginRequirement, PulpEntityContext
55
from pulp_glue.common.i18n import get_translation
66
from pulp_glue.core.context import PulpArtifactContext
7-
from pulp_glue.python.context import PulpPythonContentContext, PulpPythonRepositoryContext
7+
from pulp_glue.python.context import (
8+
PulpPythonContentContext,
9+
PulpPythonProvenanceContext,
10+
PulpPythonRepositoryContext,
11+
)
812

913
from pulp_cli.generic import (
1014
PulpCLIContext,
@@ -14,6 +18,7 @@
1418
label_command,
1519
label_select_option,
1620
list_command,
21+
load_json_callback,
1722
pass_entity_context,
1823
pass_pulp_context,
1924
pulp_group,
@@ -37,6 +42,24 @@ def _sha256_artifact_callback(
3742
return value
3843

3944

45+
def _attestation_callback(
46+
ctx: click.Context, param: click.Parameter, value: t.Iterable[str] | None
47+
) -> list[t.Any] | None:
48+
"""Callback to process multiple attestation values and combine them into a list."""
49+
if not value:
50+
return None
51+
result = []
52+
for attestation_value in value:
53+
# Use load_json_callback to process each value (supports JSON strings and file paths)
54+
processed = load_json_callback(ctx, param, attestation_value)
55+
# If it's already a list, extend; otherwise append
56+
if isinstance(processed, list):
57+
result.extend(processed)
58+
else:
59+
result.append(processed)
60+
return result
61+
62+
4063
repository_option = resource_option(
4164
"--repository",
4265
default_plugin="python",
@@ -51,26 +74,44 @@ def _sha256_artifact_callback(
5174
),
5275
)
5376

77+
package_option = resource_option(
78+
"--package",
79+
default_plugin="python",
80+
default_type="package",
81+
lookup_key="sha256",
82+
context_table={
83+
"python:package": PulpPythonContentContext,
84+
},
85+
href_pattern=PulpPythonContentContext.HREF_PATTERN,
86+
help=_(
87+
"Package to associate the provenance with in the form '[[<plugin>:]<resource_type>:]<sha256>' or by href/prn."
88+
),
89+
allowed_with_contexts=(PulpPythonProvenanceContext,),
90+
required=True,
91+
)
92+
5493

5594
@pulp_group()
5695
@click.option(
5796
"-t",
5897
"--type",
5998
"content_type",
60-
type=click.Choice(["package"], case_sensitive=False),
99+
type=click.Choice(["package", "provenance"], case_sensitive=False),
61100
default="package",
62101
)
63102
@pass_pulp_context
64103
@click.pass_context
65104
def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str) -> None:
66105
if content_type == "package":
67106
ctx.obj = PulpPythonContentContext(pulp_ctx)
107+
elif content_type == "provenance":
108+
ctx.obj = PulpPythonProvenanceContext(pulp_ctx)
68109
else:
69110
raise NotImplementedError()
70111

71112

72113
create_options = [
73-
click.option("--relative-path", required=True, help=_("Exact name of file")),
114+
pulp_option("--relative-path", required=True, help=_("Exact name of file"), allowed_with_contexts=(PulpPythonContentContext,)),
74115
click.option(
75116
"--sha256",
76117
"artifact",
@@ -79,21 +120,43 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
79120
),
80121
pulp_option(
81122
"--file-url",
82-
help=_("Remote url to download and create python content from"),
123+
help=_("Remote url to download and create {entity} from"),
83124
needs_plugins=[PluginRequirement("core", specifier=">=3.56.1")],
84125
),
126+
pulp_option(
127+
"--attestation",
128+
"attestations",
129+
multiple=True,
130+
callback=_attestation_callback,
131+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
132+
help=_(
133+
"A JSON object containing an attestation for the package. Can be a JSON string or a file path prefixed with '@'. Can be specified multiple times."
134+
),
135+
allowed_with_contexts=(PulpPythonContentContext,),
136+
),
137+
]
138+
provenance_create_options = [
139+
pulp_option("--file", type=click.File("rb"), help=_("Provenance JSON file"), allowed_with_contexts=(PulpPythonProvenanceContext,)),
140+
package_option,
141+
pulp_option(
142+
"--verify/--no-verify",
143+
default=True,
144+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
145+
help=_("Verify the provenance"),
146+
allowed_with_contexts=(PulpPythonProvenanceContext,),
147+
),
85148
]
86149
lookup_options = [href_option]
87150
content.add_command(
88151
list_command(
89152
decorators=[
90-
click.option("--filename", type=str),
153+
pulp_option("--filename", type=str, allowed_with_contexts=(PulpPythonContentContext,)),
91154
label_select_option,
92155
]
93156
)
94157
)
95158
content.add_command(show_command(decorators=lookup_options))
96-
content.add_command(create_command(decorators=create_options))
159+
content.add_command(create_command(decorators=create_options + provenance_create_options))
97160
content.add_command(
98161
label_command(
99162
decorators=lookup_options,
@@ -102,10 +165,20 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
102165
)
103166

104167

105-
@content.command()
168+
@content.command(allowed_with_contexts=(PulpPythonContentContext,))
106169
@click.option("--relative-path", required=True, help=_("Exact name of file"))
107170
@click.option("--file", type=click.File("rb"), required=True, help=_("Path to file"))
108171
@chunk_size_option
172+
@pulp_option(
173+
"--attestation",
174+
"attestations",
175+
multiple=True,
176+
callback=_attestation_callback,
177+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
178+
help=_(
179+
"A JSON object containing an attestation for the package. Can be a JSON string or a file path prefixed with '@'. Can be specified multiple times."
180+
),
181+
)
109182
@repository_option
110183
@pass_entity_context
111184
@pass_pulp_context
@@ -116,12 +189,17 @@ def upload(
116189
relative_path: str,
117190
file: t.IO[bytes],
118191
chunk_size: int,
192+
attestations: list[t.Any] | None,
119193
repository: PulpPythonRepositoryContext | None,
120194
) -> None:
121195
"""Create a Python package content unit through uploading a file"""
122196
assert isinstance(entity_ctx, PulpPythonContentContext)
123197

124198
result = entity_ctx.upload(
125-
relative_path=relative_path, file=file, chunk_size=chunk_size, repository=repository
199+
relative_path=relative_path,
200+
file=file,
201+
chunk_size=chunk_size,
202+
repository=repository,
203+
attestations=attestations,
126204
)
127205
pulp_ctx.output_result(result)

pulpcore/cli/python/remote.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def remote(ctx: click.Context, pulp_ctx: PulpCLIContext, /, remote_type: str) ->
9898
callback=load_json_callback,
9999
needs_plugins=[PluginRequirement("python", specifier=">=3.2.0")],
100100
),
101+
click.option("--provenance", type=click.BOOL, default=False, help=_("Sync available package provenances")),
101102
]
102103

103104
remote.add_command(list_command(decorators=remote_filter_options))

pulpcore/cli/python/repository.py

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pulp_glue.common.i18n import get_translation
1212
from pulp_glue.python.context import (
1313
PulpPythonContentContext,
14+
PulpPythonProvenanceContext,
1415
PulpPythonRemoteContext,
1516
PulpPythonRepositoryContext,
1617
)
@@ -22,11 +23,10 @@
2223
create_content_json_callback,
2324
destroy_command,
2425
href_option,
25-
json_callback,
2626
label_command,
2727
label_select_option,
2828
list_command,
29-
load_file_wrapper,
29+
lookup_callback,
3030
name_option,
3131
pass_pulp_context,
3232
pass_repository_context,
@@ -60,31 +60,7 @@
6060
)
6161

6262

63-
def _content_callback(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any:
64-
if value:
65-
pulp_ctx = ctx.find_object(PulpCLIContext)
66-
assert pulp_ctx is not None
67-
ctx.obj = PulpPythonContentContext(pulp_ctx, entity=value)
68-
return value
69-
70-
71-
CONTENT_LIST_SCHEMA = s.Schema([{"sha256": str, "filename": s.And(str, len)}])
72-
73-
74-
@load_file_wrapper
75-
def _content_list_callback(ctx: click.Context, param: click.Parameter, value: str | None) -> t.Any:
76-
if value is None:
77-
return None
78-
79-
result = json_callback(ctx, param, value)
80-
try:
81-
return CONTENT_LIST_SCHEMA.validate(result)
82-
except s.SchemaError as e:
83-
raise click.ClickException(
84-
_("Validation of '{parameter}' failed: {error}").format(
85-
parameter=param.name, error=str(e)
86-
)
87-
)
63+
CONTENT_LIST_SCHEMA = s.Schema([{"sha256": str}])
8864

8965

9066
@pulp_group()
@@ -119,36 +95,34 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str)
11995
]
12096
create_options = update_options + [click.option("--name", required=True)]
12197
package_options = [
122-
click.option("--sha256", cls=GroupOption, expose_value=False, group=["filename"]),
123-
click.option(
124-
"--filename",
125-
callback=_content_callback,
98+
pulp_option(
99+
"--sha256",
100+
callback=lookup_callback("sha256"),
126101
expose_value=False,
127-
cls=GroupOption,
128-
group=["sha256"],
129-
help=_("Filename of the python package."),
102+
help=_("SHA256 digest of the {entity}."),
130103
),
104+
href_option,
131105
]
132106
content_json_callback = create_content_json_callback(
133-
PulpPythonContentContext, schema=CONTENT_LIST_SCHEMA
107+
None, schema=CONTENT_LIST_SCHEMA
134108
)
135109
modify_options = [
136-
click.option(
110+
pulp_option(
137111
"--add-content",
138112
callback=content_json_callback,
139113
help=_(
140-
"""JSON string with a list of objects to add to the repository.
141-
Each object must contain the following keys: "sha256", "filename".
142-
The argument prefixed with the '@' can be the path to a JSON file with a list of objects."""
114+
"""JSON string with a list of {entities} to add to the repository.
115+
Each {entity} must contain the following keys: "sha256".
116+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
143117
),
144118
),
145-
click.option(
119+
pulp_option(
146120
"--remove-content",
147121
callback=content_json_callback,
148122
help=_(
149-
"""JSON string with a list of objects to remove from the repository.
150-
Each object must contain the following keys: "sha256", "filename".
151-
The argument prefixed with the '@' can be the path to a JSON file with a list of objects."""
123+
"""JSON string with a list of {entities} to remove from the repository.
124+
Each {entity} must contain the following keys: "sha256".
125+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
152126
),
153127
),
154128
]
@@ -163,7 +137,10 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str)
163137
repository.add_command(label_command(decorators=nested_lookup_options))
164138
repository.add_command(
165139
repository_content_command(
166-
contexts={"package": PulpPythonContentContext},
140+
contexts={
141+
"package": PulpPythonContentContext,
142+
"provenance": PulpPythonProvenanceContext,
143+
},
167144
add_decorators=package_options,
168145
remove_decorators=package_options,
169146
modify_decorators=modify_options,

0 commit comments

Comments
 (0)