Skip to content

Commit 9e72c96

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

File tree

8 files changed

+151
-55
lines changed

8 files changed

+151
-55
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: 99 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,50 @@ 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"
88+
"'[[<plugin>:]<resource_type>:]<sha256>' or by href/prn."
89+
),
90+
allowed_with_contexts=(PulpPythonProvenanceContext,),
91+
required=True,
92+
)
93+
5494

5595
@pulp_group()
5696
@click.option(
5797
"-t",
5898
"--type",
5999
"content_type",
60-
type=click.Choice(["package"], case_sensitive=False),
100+
type=click.Choice(["package", "provenance"], case_sensitive=False),
61101
default="package",
62102
)
63103
@pass_pulp_context
64104
@click.pass_context
65105
def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str) -> None:
66106
if content_type == "package":
67107
ctx.obj = PulpPythonContentContext(pulp_ctx)
108+
elif content_type == "provenance":
109+
ctx.obj = PulpPythonProvenanceContext(pulp_ctx)
68110
else:
69111
raise NotImplementedError()
70112

71113

72114
create_options = [
73-
click.option("--relative-path", required=True, help=_("Exact name of file")),
115+
pulp_option(
116+
"--relative-path",
117+
required=True,
118+
help=_("Exact name of file"),
119+
allowed_with_contexts=(PulpPythonContentContext,),
120+
),
74121
click.option(
75122
"--sha256",
76123
"artifact",
@@ -79,21 +126,49 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
79126
),
80127
pulp_option(
81128
"--file-url",
82-
help=_("Remote url to download and create python content from"),
129+
help=_("Remote url to download and create {entity} from"),
83130
needs_plugins=[PluginRequirement("core", specifier=">=3.56.1")],
84131
),
132+
pulp_option(
133+
"--attestation",
134+
"attestations",
135+
multiple=True,
136+
callback=_attestation_callback,
137+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
138+
help=_(
139+
"A JSON object containing an attestation for the package. Can be a JSON string or a "
140+
"file path prefixed with '@'. Can be specified multiple times."
141+
),
142+
allowed_with_contexts=(PulpPythonContentContext,),
143+
),
144+
]
145+
provenance_create_options = [
146+
pulp_option(
147+
"--file",
148+
type=click.File("rb"),
149+
help=_("Provenance JSON file"),
150+
allowed_with_contexts=(PulpPythonProvenanceContext,),
151+
),
152+
package_option,
153+
pulp_option(
154+
"--verify/--no-verify",
155+
default=True,
156+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
157+
help=_("Verify the provenance"),
158+
allowed_with_contexts=(PulpPythonProvenanceContext,),
159+
),
85160
]
86161
lookup_options = [href_option]
87162
content.add_command(
88163
list_command(
89164
decorators=[
90-
click.option("--filename", type=str),
165+
pulp_option("--filename", type=str, allowed_with_contexts=(PulpPythonContentContext,)),
91166
label_select_option,
92167
]
93168
)
94169
)
95170
content.add_command(show_command(decorators=lookup_options))
96-
content.add_command(create_command(decorators=create_options))
171+
content.add_command(create_command(decorators=create_options + provenance_create_options))
97172
content.add_command(
98173
label_command(
99174
decorators=lookup_options,
@@ -102,10 +177,21 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
102177
)
103178

104179

105-
@content.command()
180+
@content.command(allowed_with_contexts=(PulpPythonContentContext,))
106181
@click.option("--relative-path", required=True, help=_("Exact name of file"))
107182
@click.option("--file", type=click.File("rb"), required=True, help=_("Path to file"))
108183
@chunk_size_option
184+
@pulp_option(
185+
"--attestation",
186+
"attestations",
187+
multiple=True,
188+
callback=_attestation_callback,
189+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
190+
help=_(
191+
"A JSON object containing an attestation for the package. Can be a JSON string or a file"
192+
" path prefixed with '@'. Can be specified multiple times."
193+
),
194+
)
109195
@repository_option
110196
@pass_entity_context
111197
@pass_pulp_context
@@ -116,12 +202,17 @@ def upload(
116202
relative_path: str,
117203
file: t.IO[bytes],
118204
chunk_size: int,
205+
attestations: list[t.Any] | None,
119206
repository: PulpPythonRepositoryContext | None,
120207
) -> None:
121208
"""Create a Python package content unit through uploading a file"""
122209
assert isinstance(entity_ctx, PulpPythonContentContext)
123210

124211
result = entity_ctx.upload(
125-
relative_path=relative_path, file=file, chunk_size=chunk_size, repository=repository
212+
relative_path=relative_path,
213+
file=file,
214+
chunk_size=chunk_size,
215+
repository=repository,
216+
attestations=attestations,
126217
)
127218
pulp_ctx.output_result(result)

pulpcore/cli/python/remote.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ 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(
102+
"--provenance", type=click.BOOL, default=False, help=_("Sync available package provenances")
103+
),
101104
]
102105

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

pulpcore/cli/python/repository.py

Lines changed: 21 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,21 @@
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
)
1718

1819
from pulp_cli.generic import (
19-
GroupOption,
2020
PulpCLIContext,
2121
create_command,
2222
create_content_json_callback,
2323
destroy_command,
2424
href_option,
25-
json_callback,
2625
label_command,
2726
label_select_option,
2827
list_command,
29-
load_file_wrapper,
28+
lookup_callback,
3029
name_option,
3130
pass_pulp_context,
3231
pass_repository_context,
@@ -60,31 +59,7 @@
6059
)
6160

6261

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-
)
62+
CONTENT_LIST_SCHEMA = s.Schema([{"sha256": str}])
8863

8964

9065
@pulp_group()
@@ -119,36 +94,32 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str)
11994
]
12095
create_options = update_options + [click.option("--name", required=True)]
12196
package_options = [
122-
click.option("--sha256", cls=GroupOption, expose_value=False, group=["filename"]),
123-
click.option(
124-
"--filename",
125-
callback=_content_callback,
97+
pulp_option(
98+
"--sha256",
99+
callback=lookup_callback("sha256"),
126100
expose_value=False,
127-
cls=GroupOption,
128-
group=["sha256"],
129-
help=_("Filename of the python package."),
101+
help=_("SHA256 digest of the {entity}."),
130102
),
103+
href_option,
131104
]
132-
content_json_callback = create_content_json_callback(
133-
PulpPythonContentContext, schema=CONTENT_LIST_SCHEMA
134-
)
105+
content_json_callback = create_content_json_callback(None, schema=CONTENT_LIST_SCHEMA)
135106
modify_options = [
136-
click.option(
107+
pulp_option(
137108
"--add-content",
138109
callback=content_json_callback,
139110
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."""
111+
"""JSON string with a list of {entities} to add to the repository.
112+
Each {entity} must contain the following keys: "sha256".
113+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
143114
),
144115
),
145-
click.option(
116+
pulp_option(
146117
"--remove-content",
147118
callback=content_json_callback,
148119
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."""
120+
"""JSON string with a list of {entities} to remove from the repository.
121+
Each {entity} must contain the following keys: "sha256".
122+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
152123
),
153124
),
154125
]
@@ -163,7 +134,10 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str)
163134
repository.add_command(label_command(decorators=nested_lookup_options))
164135
repository.add_command(
165136
repository_content_command(
166-
contexts={"package": PulpPythonContentContext},
137+
contexts={
138+
"package": PulpPythonContentContext,
139+
"provenance": PulpPythonProvenanceContext,
140+
},
167141
add_decorators=package_options,
168142
remove_decorators=package_options,
169143
modify_decorators=modify_options,

0 commit comments

Comments
 (0)