Skip to content

Commit e976363

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

File tree

9 files changed

+168
-67
lines changed

9 files changed

+168
-67
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_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: 105 additions & 19 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,12 +18,14 @@
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,
2025
pulp_option,
2126
resource_option,
2227
show_command,
28+
type_option,
2329
)
2430

2531
translation = get_translation(__package__)
@@ -37,6 +43,24 @@ def _sha256_artifact_callback(
3743
return value
3844

3945

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

78+
package_option = resource_option(
79+
"--package",
80+
default_plugin="python",
81+
default_type="package",
82+
lookup_key="sha256",
83+
context_table={
84+
"python:package": PulpPythonContentContext,
85+
},
86+
href_pattern=PulpPythonContentContext.HREF_PATTERN,
87+
help=_(
88+
"Package to associate the provenance with in the form"
89+
"'[[<plugin>:]<resource_type>:]<sha256>' or by href/prn."
90+
),
91+
allowed_with_contexts=(PulpPythonProvenanceContext,),
92+
required=True,
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+
),
74116
click.option(
75117
"--sha256",
76118
"artifact",
@@ -79,21 +121,49 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
79121
),
80122
pulp_option(
81123
"--file-url",
82-
help=_("Remote url to download and create python content from"),
124+
help=_("Remote url to download and create {entity} from"),
83125
needs_plugins=[PluginRequirement("core", specifier=">=3.56.1")],
84126
),
127+
pulp_option(
128+
"--attestation",
129+
"attestations",
130+
multiple=True,
131+
callback=_attestation_callback,
132+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
133+
help=_(
134+
"A JSON object containing an attestation for the package. Can be a JSON string or a "
135+
"file path prefixed with '@'. Can be specified multiple times."
136+
),
137+
allowed_with_contexts=(PulpPythonContentContext,),
138+
),
139+
]
140+
provenance_create_options = [
141+
pulp_option(
142+
"--file",
143+
type=click.File("rb"),
144+
help=_("Provenance JSON file"),
145+
allowed_with_contexts=(PulpPythonProvenanceContext,),
146+
),
147+
package_option,
148+
pulp_option(
149+
"--verify/--no-verify",
150+
default=True,
151+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
152+
help=_("Verify the provenance"),
153+
allowed_with_contexts=(PulpPythonProvenanceContext,),
154+
),
85155
]
86156
lookup_options = [href_option]
87157
content.add_command(
88158
list_command(
89159
decorators=[
90-
click.option("--filename", type=str),
160+
pulp_option("--filename", type=str, allowed_with_contexts=(PulpPythonContentContext,)),
91161
label_select_option,
92162
]
93163
)
94164
)
95165
content.add_command(show_command(decorators=lookup_options))
96-
content.add_command(create_command(decorators=create_options))
166+
content.add_command(create_command(decorators=create_options + provenance_create_options))
97167
content.add_command(
98168
label_command(
99169
decorators=lookup_options,
@@ -102,10 +172,21 @@ def content(ctx: click.Context, pulp_ctx: PulpCLIContext, /, content_type: str)
102172
)
103173

104174

105-
@content.command()
175+
@content.command(allowed_with_contexts=(PulpPythonContentContext,))
106176
@click.option("--relative-path", required=True, help=_("Exact name of file"))
107177
@click.option("--file", type=click.File("rb"), required=True, help=_("Path to file"))
108178
@chunk_size_option
179+
@pulp_option(
180+
"--attestation",
181+
"attestations",
182+
multiple=True,
183+
callback=_attestation_callback,
184+
needs_plugins=[PluginRequirement("python", specifier=">=3.22.0")],
185+
help=_(
186+
"A JSON object containing an attestation for the package. Can be a JSON string or a file"
187+
" path prefixed with '@'. Can be specified multiple times."
188+
),
189+
)
109190
@repository_option
110191
@pass_entity_context
111192
@pass_pulp_context
@@ -116,12 +197,17 @@ def upload(
116197
relative_path: str,
117198
file: t.IO[bytes],
118199
chunk_size: int,
200+
attestations: list[t.Any] | None,
119201
repository: PulpPythonRepositoryContext | None,
120202
) -> None:
121203
"""Create a Python package content unit through uploading a file"""
122204
assert isinstance(entity_ctx, PulpPythonContentContext)
123205

124206
result = entity_ctx.upload(
125-
relative_path=relative_path, file=file, chunk_size=chunk_size, repository=repository
207+
relative_path=relative_path,
208+
file=file,
209+
chunk_size=chunk_size,
210+
repository=repository,
211+
attestations=attestations,
126212
)
127213
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 & 44 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,38 @@ 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"],
129106
help=_("Filename of the python package."),
107+
deprecated=True,
130108
),
109+
href_option,
131110
]
132-
content_json_callback = create_content_json_callback(
133-
PulpPythonContentContext, schema=CONTENT_LIST_SCHEMA
134-
)
111+
content_json_callback = create_content_json_callback(None, schema=CONTENT_LIST_SCHEMA)
135112
modify_options = [
136-
click.option(
113+
pulp_option(
137114
"--add-content",
138115
callback=content_json_callback,
139116
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."""
117+
"""JSON string with a list of {entities} to add to the repository.
118+
Each {entity} must contain the following keys: "sha256".
119+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
143120
),
144121
),
145-
click.option(
122+
pulp_option(
146123
"--remove-content",
147124
callback=content_json_callback,
148125
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."""
126+
"""JSON string with a list of {entities} to remove from the repository.
127+
Each {entity} must contain the following keys: "sha256".
128+
The argument prefixed with the '@' can be the path to a JSON file with a list of {entities}."""
152129
),
153130
),
154131
]
@@ -163,7 +140,10 @@ def repository(ctx: click.Context, pulp_ctx: PulpCLIContext, /, repo_type: str)
163140
repository.add_command(label_command(decorators=nested_lookup_options))
164141
repository.add_command(
165142
repository_content_command(
166-
contexts={"package": PulpPythonContentContext},
143+
contexts={
144+
"package": PulpPythonContentContext,
145+
"provenance": PulpPythonProvenanceContext,
146+
},
167147
add_decorators=package_options,
168148
remove_decorators=package_options,
169149
modify_decorators=modify_options,

0 commit comments

Comments
 (0)