Skip to content

Commit 09bb29e

Browse files
committed
Merged PR 6563359: Python and file content support
This PR adds support for two new types of content: Python packages and files. Each can be managed by Pulp. For right now, I am referring to file content (e.g. [the clamav signatures](https://packages.microsoft.com/clamav/)) with the name `FilePackage` to be consistent with the other types of content we offer (RpmPackage, PythonPackage, DebPackage). I'm not totally happy with this name but there are two arguments for it: 1. It keeps things consistent/easy/simple 2. We (the pmc team) will probably be the only ones to manage file content so the name doesn't matter too much IMO That said, I'm open to suggestion for how to name things. Here are some examples of where/how this name manifests: ``` # schemas RpmPackageResponse DebPackageResponse PythonPackageResponse FilePackageResponse ? # api endpoints /rpm/packages/ /python/packages/ /python/packages/ /file/packages/ ? # cli commands pmc package rpm list pmc package deb list pmc package python list pmc package file list ? ``` Related work items: #15206527
1 parent 757cb21 commit 09bb29e

23 files changed

Lines changed: 445 additions & 129 deletions

cli/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
tests/settings.toml
2+
dist

cli/README.md

Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -84,66 +84,42 @@ A default Service Principal is available to simplify your dev environment
8484
- `./update_role.sh Repo_Admin --create`
8585
- You can call this script again any time you wish to change roles (`./update_role.sh Account_Admin`)
8686

87-
## Example Workflows
87+
## Workflows
8888

89-
### apt
89+
Once you've set up the server and CLI, view the `docs/admin/workflows.md` file for some example
90+
workflows.
9091

91-
```
92-
# create a repo. Note: only legacy signing is available in dev environments.
93-
pmc repo create myrepo-apt apt --signing-service legacy
94-
95-
# create a repo release
96-
pmc repo releases create myrepo-apt jammy
97-
98-
# create a distro
99-
pmc distro create mydistro-apt apt "some/path" --repository myrepo-apt
100-
101-
# upload a package
102-
cp tests/assets/signed-by-us.deb .
103-
PACKAGE_ID=$(pmc --id-only package upload signed-by-us.deb)
92+
## Publishing
10493

105-
# add our package to the repo release
106-
pmc repo packages update myrepo-apt jammy --add-packages $PACKAGE_ID
94+
### Server Setup
10795

108-
# publish the repo
109-
pmc repo publish myrepo-apt
96+
These steps describe how to prepare the PMC server to distribute the CLI package. They assume that
97+
your pmc client is setup for the server and that you want to distribute the package at `pypi`.
11098

111-
# check out our repo
112-
http :8081/pulp/content/some/path/
11399
```
100+
# create a repo named pypi-python
101+
pmc repo create pypi-python python
114102
115-
### yum
116-
103+
# create a distro pypi that serves from a folder 'pypi'
104+
pmc distro create pypi pypi pypi --repository pypi-python
117105
```
118-
# create a repo. Note: only legacy signing is available in dev environments.
119-
pmc repo create myrepo-yum yum --signing-service legacy
120-
121-
# create a distro
122-
pmc distro create mydistro-yum yum "awesome/path" --repository myrepo-yum
123106

124-
# upload a package
125-
cp tests/assets/signed-by-us.rpm .
126-
PACKAGE_ID=$(pmc --id-only package upload signed-by-us.deb)
107+
### Packaging
127108

128-
# add our package to the repo release
129-
pmc repo packages update myrepo-yum --add-packages $PACKAGE_ID
109+
1. Open up pyproject.toml file and confirm that the version field is correct.
110+
2. If it's not correct, update it and open a new PR with your change.
111+
3. In the cli directory, run `poetry build`.
112+
4. Now proceed to the next section to upload your CLI package.
130113

131-
# publish the repo
132-
pmc repo publish myrepo-yum
133-
134-
# check out our repo
135-
http :8081/pulp/content/awesome/path/
136-
```
114+
### Uploading
137115

138-
### syncing
116+
These steps assume that your pmc client is set up for the server from which you want to distribute
117+
the pmc cli package.
139118

140119
```
141-
# create a remote
142-
pmc remote create microsoft-ubuntu-focal-prod apt "https://packages.microsoft.com/repos/microsoft-ubuntu-focal-prod/" --distributions nightly
120+
PACKAGE_ID=$(pmc package upload dist/pmc_cli-0.0.1-py3-none-any.whl)
143121
144-
# create a repo
145-
pmc repo create microsoft-ubuntu-focal-prod apt --remote microsoft-ubuntu-focal-prod
122+
pmc repo packages update pypi-python --add-packages $PACKAGE_ID
146123
147-
# sync
148-
pmc repo sync microsoft-ubuntu-focal-prod
124+
pmc repo publish pypi-python
149125
```

cli/pmc/commands/package.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict
1+
from typing import Any, Dict, Optional
22

33
import typer
44

@@ -9,15 +9,19 @@
99
app = UserFriendlyTyper()
1010
deb = UserFriendlyTyper()
1111
rpm = UserFriendlyTyper()
12+
python = UserFriendlyTyper()
13+
file = UserFriendlyTyper()
14+
1215
app.add_typer(deb, name="deb", help="Manage deb packages")
1316
app.add_typer(rpm, name="rpm", help="Manage rpm packages")
17+
app.add_typer(python, name="python", help="Manage python packages")
18+
app.add_typer(file, name="file", help="Manage files")
1419

1520

16-
def _list(type: PackageType, ctx: typer.Context, limit: int, offset: int) -> None:
21+
def _list(package_type: PackageType, ctx: typer.Context, limit: int, offset: int) -> None:
1722
params: Dict[str, Any] = dict(limit=limit, offset=offset)
18-
1923
with get_client(ctx.obj) as client:
20-
resp = client.get(f"/{type}/packages/", params=params)
24+
resp = client.get(f"/{package_type}/packages/", params=params)
2125
handle_response(ctx.obj, resp)
2226

2327

@@ -33,6 +37,18 @@ def rpm_list(ctx: typer.Context, limit: int = LIMIT_OPT, offset: int = OFFSET_OP
3337
_list(PackageType.rpm, ctx, limit, offset)
3438

3539

40+
@python.command(name="list")
41+
def python_list(ctx: typer.Context, limit: int = LIMIT_OPT, offset: int = OFFSET_OPT) -> None:
42+
"""List python packages."""
43+
_list(PackageType.python, ctx, limit, offset)
44+
45+
46+
@file.command(name="list")
47+
def file_list(ctx: typer.Context, limit: int = LIMIT_OPT, offset: int = OFFSET_OPT) -> None:
48+
"""List files."""
49+
_list(PackageType.file, ctx, limit, offset)
50+
51+
3652
@app.command()
3753
def upload(
3854
ctx: typer.Context,
@@ -43,6 +59,15 @@ def upload(
4359
show_default=False,
4460
help="Ignore the signature check. Only allowable for legacy packages.",
4561
),
62+
file_type: Optional[PackageType] = typer.Option(
63+
None,
64+
"--type",
65+
"-t",
66+
help=(
67+
"Manually specify the type of file being uploaded. Otherwise the file's extension "
68+
"is used to guess the file type."
69+
),
70+
),
4671
) -> None:
4772
"""Upload a package."""
4873

@@ -52,7 +77,9 @@ def show_func(task: Any) -> Any:
5277
with get_client(ctx.obj) as client:
5378
return client.get(f"/packages/{package_id}/")
5479

55-
data = {"ignore_signature": ignore_signature}
80+
data: Dict[str, Any] = {"ignore_signature": ignore_signature}
81+
if file_type:
82+
data["file_type"] = file_type
5683
files = {"file": file}
5784
with get_client(ctx.obj) as client:
5885
resp = client.post("/packages/", params=data, files=files)

cli/pmc/commands/repository.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,6 @@
2525
)
2626

2727

28-
def _signing_service_default(
29-
ctx: typer.Context, value: Optional[RepoSigningService]
30-
) -> RepoSigningService:
31-
# value will be the default if not user specified so check ctx.params instead
32-
if service_val := ctx.params.get("signing_service"):
33-
# use the user input
34-
service = service_val
35-
elif service_default := (ctx.find_root().default_map or {}).get("signing_service"):
36-
# use the config value
37-
service = service_default
38-
else:
39-
# default to esrp
40-
service = RepoSigningService.esrp
41-
42-
return RepoSigningService(service)
43-
44-
4528
@app.command()
4629
def list(
4730
ctx: typer.Context,
@@ -65,7 +48,8 @@ def create(
6548
name: str,
6649
repo_type: RepoType,
6750
signing_service: Optional[RepoSigningService] = typer.Option(
68-
RepoSigningService.esrp, callback=_signing_service_default
51+
None,
52+
help="Signing service to use for the repo. Defaults to 'esrp' for yum and apt repos.",
6953
),
7054
remote: Optional[str] = id_or_name(
7155
"remotes", typer.Option(None, help="Remote id or name to use for sync.")
@@ -75,7 +59,19 @@ def create(
7559
),
7660
) -> None:
7761
"""Create a repository."""
78-
data = {"name": name, "type": repo_type, "signing_service": signing_service, "remote": remote}
62+
data = {"name": name, "type": repo_type, "remote": remote}
63+
64+
# set signing service
65+
if repo_type in [RepoType.yum, RepoType.apt]:
66+
if signing_service:
67+
service = signing_service
68+
elif service_default := (ctx.find_root().default_map or {}).get("signing_service"):
69+
service = service_default
70+
else:
71+
service = RepoSigningService.esrp
72+
73+
data["signing_service"] = service
74+
7975
with get_client(ctx.obj) as client:
8076
repo_resp = client.post("/repositories/", json=data)
8177
handle_response(ctx.obj, repo_resp)

cli/pmc/schemas.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ class RepoType(StringEnum):
4040

4141
apt = "apt"
4242
yum = "yum" # maps to 'rpm' in Pulp
43+
python = "python"
44+
file = "file"
4345

4446

4547
class RepoSigningService(StringEnum):
@@ -54,6 +56,8 @@ class DistroType(StringEnum):
5456

5557
apt = "apt"
5658
yum = "yum" # maps to 'rpm' in Pulp
59+
python = "pypi"
60+
file = "file"
5761

5862

5963
class RemoteType(StringEnum):
@@ -68,6 +72,8 @@ class PackageType(StringEnum):
6872

6973
deb = "deb"
7074
rpm = "rpm"
75+
python = "python"
76+
file = "file"
7177

7278

7379
class Format(StringEnum):

cli/tests/assets/hello.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
world
1.93 KB
Binary file not shown.
1.59 KB
Binary file not shown.

cli/tests/commands/test_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_config_with_invalid_value() -> None:
1919
config.flush()
2020
result = invoke_command(["--config", config.name, "repo", "list"])
2121
assert result.exit_code == 1
22-
assert "DecodeError" in result.stdout
22+
assert "ValidationError" in result.stdout
2323

2424

2525
@pytest.mark.skip(reason="Authentication is required for all commands")

cli/tests/commands/test_package.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import json
2-
from typing import Any
2+
from pathlib import Path
3+
from typing import Any, Optional
34

45
import pytest
56

@@ -10,7 +11,22 @@
1011
# Note that "package upload" is exercised by fixture.
1112

1213

13-
def _assert_package_list_not_empty(type: str) -> None:
14+
def test_upload_file_type(orphan_cleanup: None) -> None:
15+
become(Role.Package_Admin)
16+
17+
# file without file type
18+
path = Path.cwd() / "tests" / "assets" / "hello.txt"
19+
result = invoke_command(["package", "upload", str(path)])
20+
assert result.exit_code != 0
21+
assert "Unrecognized file extension" in result.stdout
22+
23+
# python with file type
24+
path = Path.cwd() / "tests" / "assets" / "helloworld-0.0.1.tar.gz"
25+
result = invoke_command(["package", "upload", "--type", "python", str(path)])
26+
assert result.exit_code == 0
27+
28+
29+
def _assert_package_list_not_empty(type: str, file_type: Optional[str] = None) -> None:
1430
result = invoke_command(["package", type, "list"])
1531
assert result.exit_code == 0
1632
response = json.loads(result.stdout)
@@ -25,6 +41,14 @@ def test_rpm_list(rpm_package: Any) -> None:
2541
_assert_package_list_not_empty("rpm")
2642

2743

44+
def test_file_list(file_package: Any) -> None:
45+
_assert_package_list_not_empty("file", "file")
46+
47+
48+
def test_python_list(python_package: Any) -> None:
49+
_assert_package_list_not_empty("python")
50+
51+
2852
def test_show(deb_package: Any) -> None:
2953
result = invoke_command(["package", "show", deb_package["id"]])
3054
assert result.exit_code == 0

0 commit comments

Comments
 (0)