Skip to content

Commit

Permalink
Replace conflicting repository-service-tuf dep
Browse files Browse the repository at this point in the history
Previously, `repository-service-tuf` (i.e. the RSTUF cli) was used to
bootstrap an RSTUF repo for development. This PR re-implements the
relevant parts of the cli locally in Warehouse and removes the
`repository-service-tuf` dependency, which conflicts with other
dependencies.

Change details
- Add lightweight RSTUF API client library (can be re-used for pypi#15815)
- Add local `warehouse tuf bootstrap` cli subcommand, to wraps lib calls
- Invoke local cli via `make inittuf`
- Remove dependency

supersedes pypi#15958 (cc @facutuesca @woodruffw)

Signed-off-by: Lukas Puehringer <[email protected]>
  • Loading branch information
lukpueh committed Jun 13, 2024
1 parent 4f238c5 commit a732acd
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ initdb: .state/docker-build-base .state/db-populated
inittuf: .state/db-migrated
docker compose up -d rstuf-api
docker compose up -d rstuf-worker
docker compose run --rm web rstuf admin ceremony -b -u -f dev/rstuf/bootstrap.json --api-server http://rstuf-api
docker compose run --rm web python -m warehouse tuf bootstrap dev/rstuf/bootstrap.json --api-server http://rstuf-api

runmigrations: .state/docker-build-base
docker compose run --rm web python -m warehouse db upgrade head
Expand Down
1 change: 0 additions & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ hupper>=1.9
pip-tools>=1.0
pyramid_debugtoolbar>=2.5
pip-api
repository-service-tuf
33 changes: 33 additions & 0 deletions warehouse/cli/tuf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json

import click

from warehouse.cli import warehouse
from warehouse.tuf import post_bootstrap, wait_for_success


@warehouse.group()
def tuf():
"""Manage TUF."""


@tuf.command()
@click.argument("payload", type=click.File("rb"), required=True)
@click.option("--api-server", required=True)
def bootstrap(payload, api_server):
"""Use payload file to bootstrap RSTUF server."""
task_id = post_bootstrap(api_server, json.load(payload))
wait_for_success(api_server, task_id)
print(f"Bootstrap completed using `{payload.name}`. 🔐 🎉")
74 changes: 74 additions & 0 deletions warehouse/tuf/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
RSTUF API client library
"""

import time

from typing import Any

import requests


class RSTUFError(Exception):
pass


def get_task_state(server: str, task_id: str) -> str:
resp = requests.get(f"{server}/api/v1/task?task_id={task_id}")
resp.raise_for_status()
return resp.json()["data"]["state"]


def post_bootstrap(server: str, payload: Any) -> str:
resp = requests.post(f"{server}/api/v1/bootstrap", json=payload)
resp.raise_for_status()

# TODO: Ask upstream to not return 200 on error
resp_json = resp.json()
resp_data = resp_json.get("data")
if not resp_data:
raise RSTUFError(f"Error in RSTUF job: {resp_json}")

return resp_data["task_id"]


def wait_for_success(server: str, task_id: str):
"""Poll RSTUF task state API until success or error."""

retries = 20
delay = 1

for _ in range(retries):
state = get_task_state(server, task_id)

match state:
case "SUCCESS":
break

case "PENDING" | "RUNNING" | "RECEIVED" | "STARTED":
time.sleep(delay)
continue

case "FAILURE":
raise RSTUFError("RSTUF job failed, please check payload and retry")

case "ERRORED" | "REVOKED" | "REJECTED":
raise RSTUFError("RSTUF internal problem, please check RSTUF health")

case _:
raise RSTUFError(f"RSTUF job returned unexpected state: {state}")

else:
raise RSTUFError("RSTUF job failed, please check payload and retry")

0 comments on commit a732acd

Please sign in to comment.