Skip to content

Commit 2f01281

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

File tree

9 files changed

+169
-68
lines changed

9 files changed

+169
-68
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.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Change the python repository content add/remove/modify commands to only require the package's sha256.

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_package_provenance_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: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import json
12
import typing as t
23

34
import click
45
from pulp_glue.common.context import PluginRequirement, PulpEntityContext
56
from pulp_glue.common.i18n import get_translation
67
from pulp_glue.core.context import PulpArtifactContext
7-
from pulp_glue.python.context import PulpPythonContentContext, PulpPythonRepositoryContext
8+
from pulp_glue.python.context import (
9+
PulpPythonContentContext,
10+
PulpPythonProvenanceContext,
11+
PulpPythonRepositoryContext,
12+
)
813

914
from pulp_cli.generic import (
1015
PulpCLIContext,
@@ -14,12 +19,14 @@
1419
label_command,
1520
label_select_option,
1621
list_command,
22+
load_json_callback,
1723
pass_entity_context,
1824
pass_pulp_context,
1925
pulp_group,
2026
pulp_option,
2127
resource_option,
2228
show_command,
29+
type_option,
2330
)
2431

2532
translation = get_translation(__package__)
@@ -37,6 +44,24 @@ def _sha256_artifact_callback(
3744
return value
3845

3946

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

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

5596
@pulp_group()
56-
@click.option(
57-
"-t",
58-
"--type",
59-
"content_type",
60-
type=click.Choice(["package"], case_sensitive=False),
97+
@type_option(
98+
choices={
99+
"package": PulpPythonContentContext,
100+
"provenance": PulpPythonProvenanceContext,
101+
},
61102
default="package",
103+
case_sensitive=False,
62104
)
63-
@pass_pulp_context
64-
@click.pass_context
65-
def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str) -> None:
66-
if content_type == "package":
67-
ctx.obj = PulpPythonContentContext(pulp_ctx)
68-
else:
69-
raise NotImplementedError()
105+
def content() -> None:
106+
pass
70107

71108

72109
create_options = [
73-
click.option("--relative-path", required=True, help=_("Exact name of file")),
110+
pulp_option(
111+
"--relative-path",
112+
required=True,
113+
help=_("Exact name of file"),
114+
allowed_with_contexts=(PulpPythonContentContext,),
115+
),
116+
pulp_option(
117+
"--file",
118+
type=click.File("rb"),
119+
help=_("Path to the file to create {entity} from"),
120+
),
74121
click.option(
75122
"--sha256",
76123
"artifact",
@@ -79,21 +126,45 @@ 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+
package_option,
147+
pulp_option(
148+
"--verify/--no-verify",
149+
default=True,
150+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
151+
help=_("Verify the provenance"),
152+
allowed_with_contexts=(PulpPythonProvenanceContext,),
153+
),
85154
]
86155
lookup_options = [href_option]
87156
content.add_command(
88157
list_command(
89158
decorators=[
90-
click.option("--filename", type=str),
159+
pulp_option("--filename", allowed_with_contexts=(PulpPythonContentContext,)),
160+
pulp_option("--sha256"),
91161
label_select_option,
162+
package_option,
92163
]
93164
)
94165
)
95166
content.add_command(show_command(decorators=lookup_options))
96-
content.add_command(create_command(decorators=create_options))
167+
content.add_command(create_command(decorators=create_options + provenance_create_options))
97168
content.add_command(
98169
label_command(
99170
decorators=lookup_options,
@@ -102,10 +173,21 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
102173
)
103174

104175

105-
@content.command()
176+
@content.command(allowed_with_contexts=(PulpPythonContentContext,))
106177
@click.option("--relative-path", required=True, help=_("Exact name of file"))
107178
@click.option("--file", type=click.File("rb"), required=True, help=_("Path to file"))
108179
@chunk_size_option
180+
@pulp_option(
181+
"--attestation",
182+
"attestations",
183+
multiple=True,
184+
callback=_attestation_callback,
185+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
186+
help=_(
187+
"A JSON object containing an attestation for the package. Can be a JSON string or a file"
188+
" path prefixed with '@'. Can be specified multiple times."
189+
),
190+
)
109191
@repository_option
110192
@pass_entity_context
111193
@pass_pulp_context
@@ -116,12 +198,17 @@ def upload(
116198
relative_path: str,
117199
file: t.IO[bytes],
118200
chunk_size: int,
201+
attestations: list[t.Any] | None,
119202
repository: PulpPythonRepositoryContext | None,
120203
) -> None:
121204
"""Create a Python package content unit through uploading a file"""
122205
assert isinstance(entity_ctx, PulpPythonContentContext)
123206

124207
result = entity_ctx.upload(
125-
relative_path=relative_path, file=file, chunk_size=chunk_size, repository=repository
208+
relative_path=relative_path,
209+
file=file,
210+
chunk_size=chunk_size,
211+
repository=repository,
212+
attestations=attestations,
126213
)
127214
pulp_ctx.output_result(result)

pulpcore/cli/python/remote.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ 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+
pulp_option(
102+
"--provenance/--no-provenance",
103+
default=None,
104+
help=_("Sync available package provenances"),
105+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
106+
),
101107
]
102108

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

pulpcore/cli/python/repository.py

Lines changed: 24 additions & 45 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, s.Optional("filename"): str}])
8863

8964

9065
@pulp_group()
@@ -119,36 +94,37 @@ 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"]),
97+
pulp_option(
98+
"--sha256",
99+
callback=lookup_callback("sha256"),
100+
expose_value=False,
101+
help=_("SHA256 digest of the {entity}."),
102+
),
123103
click.option(
124104
"--filename",
125-
callback=_content_callback,
126105
expose_value=False,
127-
cls=GroupOption,
128-
group=["sha256"],
129-
help=_("Filename of the python package."),
106+
help=_("Filename of the python package. [deprecated]"),
130107
),
108+
href_option,
131109
]
132-
content_json_callback = create_content_json_callback(
133-
PulpPythonContentContext, schema=CONTENT_LIST_SCHEMA
134-
)
110+
content_json_callback = create_content_json_callback(None, schema=CONTENT_LIST_SCHEMA)
135111
modify_options = [
136-
click.option(
112+
pulp_option(
137113
"--add-content",
138114
callback=content_json_callback,
139115
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."""
116+
"""JSON string with a list of {entities} to add to the repository.
117+
Each {entity} must contain the following keys: "sha256".
118+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
143119
),
144120
),
145-
click.option(
121+
pulp_option(
146122
"--remove-content",
147123
callback=content_json_callback,
148124
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."""
125+
"""JSON string with a list of {entities} to remove from the repository.
126+
Each {entity} must contain the following keys: "sha256".
127+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
152128
),
153129
),
154130
]
@@ -163,7 +139,10 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str)
163139
repository.add_command(label_command(decorators=nested_lookup_options))
164140
repository.add_command(
165141
repository_content_command(
166-
contexts={"package": PulpPythonContentContext},
142+
contexts={
143+
"package": PulpPythonContentContext,
144+
"provenance": PulpPythonProvenanceContext,
145+
},
167146
add_decorators=package_options,
168147
remove_decorators=package_options,
169148
modify_decorators=modify_options,

0 commit comments

Comments
 (0)