├── .gitignore ├── AUTHORS ├── CONTRIBUTORS ├── Dockerfile.dev ├── LICENSE ├── README.md ├── aerich.ini ├── distrobuild ├── app.py ├── auth.py ├── bootstrap │ └── __init__.py ├── common │ ├── __init__.py │ └── tags.py ├── lookaside │ └── __init__.py ├── mbs │ └── __init__.py ├── middleware │ └── redis_session.py ├── models │ ├── __init__.py │ ├── batch.py │ ├── build.py │ ├── enums.py │ ├── lookaside.py │ └── package.py ├── routes │ ├── __init__.py │ ├── batches.py │ ├── bootstrap.py │ ├── builds.py │ ├── imports.py │ ├── lookaside.py │ ├── oidc.py │ └── packages.py ├── serialize │ └── __init__.py ├── session.py ├── settings.py ├── srpmproc.py └── templates │ └── not_authorized.html.j2 ├── distrobuild_scheduler ├── __init__.py ├── build_package.py ├── import_package.py ├── main.py ├── merge_scratch.py ├── periodic_tasks.py ├── sigul.py └── utils.py ├── migrations └── distrobuild │ ├── 0_20210226090638_init.sql │ ├── 1_20210303223722_packages.sql │ ├── 2_20210303223853_builds.sql │ ├── 3_20210409144010_batch.sql │ ├── 4_20210410103510_sign.sql │ ├── 5_20210410192210_lookaside.sql │ ├── 6_20210411154910_cancel.sql │ └── 6_20210417213810_scratch.sql ├── requirements.txt ├── run_scheduler.py └── ui ├── .prettierrc ├── babel.config.js ├── package.json ├── postcss.config.js ├── public └── index.html ├── src ├── api.ts ├── components │ ├── BatchList.tsx │ ├── BatchShow.tsx │ ├── BuildBatchShow.tsx │ ├── BuildBatches.tsx │ ├── BuildsList.tsx │ ├── BuildsTable.tsx │ ├── Dashboard.tsx │ ├── ImportBatchShow.tsx │ ├── ImportBatches.tsx │ ├── ImportsList.tsx │ ├── ImportsTable.tsx │ ├── Lookaside.tsx │ ├── Packages.tsx │ ├── PaginatedResourceList.tsx │ ├── Root.tsx │ └── ShowPackage.tsx ├── entrypoint.tsx ├── index.d.ts ├── misc.ts ├── styles │ ├── header.css │ └── tailwind.css └── utils │ ├── commits.tsx │ └── tags.tsx ├── tailwind.config.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | ui/node_modules 2 | ui/dist 3 | ui/yarn-error.log 4 | /README.html 5 | .#* 6 | .DS_Store 7 | .env 8 | __pycache__ 9 | *.pyc 10 | .venv 11 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Mustafa Gezen -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Mustafa Gezen -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM quay.io/centos/centos@sha256:dbbacecc49b088458781c16f3775f2a2ec7521079034a7ba499c8b0bb7f86875 # centos8.3.2011 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 The Distrobuild Authors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # distrobuild 2 | 3 | **THIS PROJECT IS STILL A WORK IN PROGRESS** 4 | 5 | ### Development 6 | 7 | ### Kerberos 8 | ``` 9 | kinit -R -kt {PATH_TO_KEYTAB} koji/distrobuild@ROCKYLINUX.ORG -S HTTP/koji.rockylinux.org@ROCKYLINUX.org 10 | ``` 11 | 12 | #### UI 13 | ``` 14 | cd ui 15 | yarn 16 | yarn start 17 | ``` 18 | 19 | #### Server 20 | ``` 21 | virtualenv .venv 22 | source .venv/bin/activate 23 | pip install -r requirements.txt 24 | # set database settings in distrobuild/settings.py 25 | aerich upgrade 26 | uvicorn distrobuild.app:app --reload --port 8090 27 | ``` 28 | 29 | #### Scheduler 30 | ``` 31 | source .venv/bin/activate 32 | python3 run_scheduler.py 33 | ``` 34 | -------------------------------------------------------------------------------- /aerich.ini: -------------------------------------------------------------------------------- 1 | [aerich] 2 | tortoise_orm = distrobuild.settings.TORTOISE_ORM 3 | location = ./migrations 4 | -------------------------------------------------------------------------------- /distrobuild/app.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import asyncio 22 | 23 | from tortoise import Tortoise 24 | 25 | Tortoise.init_models(["distrobuild.models"], "distrobuild") 26 | 27 | from fastapi import FastAPI, Request 28 | from fastapi.responses import HTMLResponse 29 | from fastapi.templating import Jinja2Templates 30 | from fastapi.staticfiles import StaticFiles 31 | from tortoise.contrib.fastapi import register_tortoise 32 | 33 | from distrobuild.middleware.redis_session import RedisSessionMiddleware 34 | from distrobuild.settings import TORTOISE_ORM, settings 35 | from distrobuild.routes import register_routes 36 | 37 | # init sessions 38 | from distrobuild import session 39 | 40 | from distrobuild_scheduler import init_channel 41 | 42 | app = FastAPI() 43 | app.add_middleware(RedisSessionMiddleware, secret_key=settings.session_secret, max_age=3000, fapi=app, 44 | https_only=settings.production) 45 | app.mount("/static/files", StaticFiles(directory="ui/dist/files"), name="static") 46 | register_routes(app) 47 | 48 | templates = Jinja2Templates(directory="ui/dist/templates") 49 | static_templates = Jinja2Templates(directory="distrobuild/templates") 50 | 51 | 52 | @app.get("/{full_path:path}", response_class=HTMLResponse, include_in_schema=False) 53 | async def serve_frontend(request: Request): 54 | not_authorized_message = request.session.get("not_authorized") 55 | if not_authorized_message: 56 | request.session.pop("not_authorized") 57 | return static_templates.TemplateResponse("not_authorized.html.j2", { 58 | "request": request, 59 | "message": not_authorized_message, 60 | }) 61 | 62 | return templates.TemplateResponse("index.html", { 63 | "request": request, 64 | "distribution": settings.distribution, 65 | "authenticated": "true" if request.session.get("user") else "false", 66 | "full_name": request.session.get("user").get("name") if request.session.get("user") else "", 67 | "koji_weburl": session.koji_config.get("weburl"), 68 | "gitlab_url": f"https://{settings.gitlab_host}", 69 | "repo_prefix": settings.repo_prefix 70 | }) 71 | 72 | 73 | @app.on_event("startup") 74 | async def startup(): 75 | await init_channel(asyncio.get_event_loop()) 76 | 77 | 78 | register_tortoise( 79 | app, 80 | config=TORTOISE_ORM 81 | ) 82 | -------------------------------------------------------------------------------- /distrobuild/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from authlib.integrations.starlette_client import OAuth 22 | 23 | from distrobuild.settings import settings 24 | 25 | oauth = OAuth() 26 | oauth.register( 27 | name="oidc", 28 | client_id=settings.oidc_client_id, 29 | client_secret=settings.oidc_client_secret, 30 | server_metadata_url=f"{settings.oidc_issuer}/.well-known/openid-configuration", 31 | client_kwargs={ 32 | "scope": f"openid profile groups {settings.oidc_scopes}" 33 | } 34 | ) 35 | 36 | oidc = oauth.oidc 37 | -------------------------------------------------------------------------------- /distrobuild/bootstrap/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import json 22 | 23 | from distrobuild.common import tags 24 | from distrobuild.models import Package, PackageModule, Repo 25 | from distrobuild.session import koji_session 26 | from distrobuild.settings import settings 27 | 28 | 29 | async def _internal_create_package(**kwargs) -> Package: 30 | return await Package.create( 31 | el8=True if settings.version == 8 else False, 32 | el9=True if settings.version == 9 else False, 33 | **kwargs, 34 | ) 35 | 36 | 37 | async def process_repo_dump(repo: Repo, responsible_username: str) -> None: 38 | with open(f"/tmp/{repo}_ALL.txt", "r") as f: 39 | lines = f.readlines() 40 | for line in lines: 41 | package_name = line.strip() 42 | existing_package = await Package.filter(name=package_name, repo__not=Repo.MODULAR_CANDIDATE).get_or_none() 43 | if existing_package and repo != Repo.MODULAR_CANDIDATE: 44 | existing_package.repo = repo 45 | existing_package.is_package = True 46 | await existing_package.save() 47 | continue 48 | 49 | is_package = True 50 | if repo == Repo.MODULAR_CANDIDATE: 51 | koji_session.packageListAdd(tags.modular_updates_candidate(), package_name, "distrobuild") 52 | is_package = False 53 | 54 | await _internal_create_package(name=package_name, 55 | is_package=is_package, 56 | repo=repo, 57 | responsible_username=responsible_username) 58 | 59 | 60 | async def process_module_dump(responsible_username: str) -> None: 61 | f = open("/tmp/modules.json") 62 | module_list = json.loads(f.read()) 63 | f.close() 64 | 65 | for module in module_list.keys(): 66 | package_name = module.strip() 67 | existing_package = await Package.filter(name=package_name, repo__not=Repo.MODULAR_CANDIDATE).get_or_none() 68 | if existing_package: 69 | if not existing_package.is_module and existing_package.repo != Repo.MODULAR_CANDIDATE: 70 | existing_package.is_module = True 71 | await existing_package.save() 72 | else: 73 | existing_package = await _internal_create_package(name=package_name, 74 | is_module=True, 75 | responsible_username=responsible_username) 76 | 77 | subpackages = [x.strip() for x in module_list[module].split(",")] 78 | for module_package in subpackages: 79 | if len(module_package.strip()) == 0: 80 | continue 81 | m_package_name = module_package.strip() 82 | m_packages = await Package.filter(name=m_package_name, is_module=False).all() 83 | 84 | for m_package in m_packages: 85 | if m_package and m_package.repo != Repo.MODULAR_CANDIDATE: 86 | m_package.part_of_module = True 87 | 88 | # Either this is safe to skip, or it will cause build failure later 89 | # This is not a bug though as all package records should be up to 90 | # date before bootstrapping the module list 91 | if not m_package: 92 | continue 93 | 94 | await m_package.save() 95 | 96 | existing_pm_count = await PackageModule.filter(package_id=m_package.id, 97 | module_parent_package_id=existing_package.id).count() 98 | if existing_pm_count == 0: 99 | await PackageModule.create(package_id=m_package.id, module_parent_package_id=existing_package.id) 100 | -------------------------------------------------------------------------------- /distrobuild/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import List, Tuple, Optional 22 | 23 | from fastapi import Request, HTTPException 24 | 25 | from distrobuild.models import Import, ImportStatus, Package, PackageModule, BatchImportPackage, Repo 26 | from distrobuild.settings import settings 27 | 28 | 29 | def gen_body_filters(body: dict) -> dict: 30 | filters = { 31 | "repo__not": Repo.MODULAR_CANDIDATE 32 | } 33 | 34 | if body.get("package_name"): 35 | filters["name"] = body["package_name"] 36 | if body.get("package_id"): 37 | filters["id"] = body["package_id"] 38 | 39 | return filters 40 | 41 | 42 | async def create_import_order(package: Package, username: str, batch_import_id: Optional[int] = None) -> \ 43 | List[Tuple[int, int]]: 44 | pkg_list = [] 45 | 46 | if package.is_package: 47 | package_import = await Import.create(package_id=package.id, status=ImportStatus.QUEUED, 48 | executor_username=username, version=settings.version) 49 | if batch_import_id: 50 | await BatchImportPackage.create(import_id=package_import.id, batch_import_id=batch_import_id) 51 | pkg_list.append((package.id, package_import.id)) 52 | 53 | if package.is_module: 54 | subpackages = await PackageModule.filter(module_parent_package_id=package.id).all() 55 | for subpackage in subpackages: 56 | imports = await Import.filter(package_id=subpackage.package_id).all() 57 | if not imports or len(imports) == 0: 58 | subpackage_package = await Package.filter(id=subpackage.package_id).get() 59 | pkg_list += await create_import_order(subpackage_package, username, batch_import_id) 60 | 61 | package_module_import = await Import.create(package_id=package.id, status=ImportStatus.QUEUED, 62 | module=True, executor_username=username, version=settings.version) 63 | if batch_import_id: 64 | await BatchImportPackage.create(import_id=package_module_import.id, batch_import_id=batch_import_id) 65 | pkg_list.append((package.id, package_module_import.id)) 66 | 67 | return pkg_list 68 | 69 | 70 | async def batch_list_check(packages, check_imports: bool = False): 71 | for package in packages: 72 | filters = {} 73 | if package.get("package_id"): 74 | filters["id"] = package["package_id"] 75 | elif package.get("package_name"): 76 | filters["name"] = package["package_name"] 77 | 78 | db_package = await Package.filter(**filters, repo__not=Repo.MODULAR_CANDIDATE).prefetch_related( 79 | "imports").first() 80 | if not db_package: 81 | detail = "" 82 | if package.get("package_id"): 83 | detail = f"Package with id {package['package_id']} not found" 84 | elif package.get("package_name"): 85 | detail = f"Package with name {package['package_name']} not found" 86 | raise HTTPException(412, detail=detail) 87 | elif check_imports and (not db_package.imports or len(db_package.imports) == 0): 88 | detail = "" 89 | if package.get("package_id"): 90 | detail = f"Package with id {package['package_id']} not imported" 91 | elif package.get("package_name"): 92 | detail = f"Package with name {package['package_name']} not imported" 93 | raise HTTPException(412, detail=detail) 94 | 95 | 96 | def get_user(request: Request) -> dict: 97 | user = request.session.get("user") 98 | token = request.session.get("token") 99 | if not user or not token: 100 | if request.session.get("token"): 101 | request.session.pop("token") 102 | if request.session.get("user"): 103 | request.session.pop("user") 104 | raise HTTPException(401, "not authenticated") 105 | 106 | return user 107 | -------------------------------------------------------------------------------- /distrobuild/common/tags.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from distrobuild.settings import settings 22 | 23 | 24 | def base() -> str: 25 | return f"dist-{settings.tag_prefix}{settings.version}" 26 | 27 | 28 | def extras() -> str: 29 | return f"{base()}-extras" 30 | 31 | 32 | def compose() -> str: 33 | return f"{base()}-compose" 34 | 35 | 36 | def module_compose() -> str: 37 | return f"{base()}-module-compose" 38 | 39 | 40 | def testing() -> str: 41 | return f"{base()}-testing" 42 | 43 | 44 | def modular_updates_candidate() -> str: 45 | return "modular-updates-candidate" 46 | -------------------------------------------------------------------------------- /distrobuild/lookaside/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from enum import Enum 22 | from typing import BinaryIO 23 | 24 | import boto3 25 | from botocore.exceptions import ClientError 26 | from google.cloud import storage 27 | 28 | 29 | class LookasideUploadException(Exception): 30 | pass 31 | 32 | 33 | class LookasideBackend(str, Enum): 34 | FILE = "file" 35 | S3 = "s3" 36 | GCS = "gcs" 37 | 38 | 39 | class Lookaside: 40 | backend: LookasideBackend 41 | 42 | def __init__(self, url: str): 43 | if url.startswith("file://"): 44 | self.backend = LookasideBackend.FILE 45 | self.dir = url[len("file://"):] 46 | elif url.startswith("s3://"): 47 | self.backend = LookasideBackend.S3 48 | self.s3 = boto3.client("s3") 49 | self.bucket = url[len("s3://"):] 50 | elif url.startswith("gs://"): 51 | self.backend = LookasideBackend.GCS 52 | self.gcs = storage.Client().bucket(url[len("gs://"):]) 53 | 54 | def upload(self, f: BinaryIO, name: str): 55 | if self.backend == LookasideBackend.FILE: 56 | file_content = f.read() 57 | with open(f"{self.dir}/{name}", "wb") as f2: 58 | f2.write(file_content) 59 | elif self.backend == LookasideBackend.S3: 60 | try: 61 | self.s3.upload_fileobj(f, self.bucket, name) 62 | except ClientError as e: 63 | raise LookasideUploadException(e) 64 | elif self.backend == LookasideBackend.GCS: 65 | blob = self.gcs.blob(name) 66 | blob.upload_from_file(f) 67 | -------------------------------------------------------------------------------- /distrobuild/mbs/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from dataclasses import dataclass 22 | 23 | import httpx 24 | 25 | from distrobuild.settings import settings 26 | 27 | 28 | class MBSConflictException(Exception): 29 | pass 30 | 31 | 32 | class MBSUnauthorizedException(Exception): 33 | pass 34 | 35 | 36 | class MBSBuildNotFound(Exception): 37 | pass 38 | 39 | 40 | @dataclass 41 | class MBSClient: 42 | mbs_url: str 43 | 44 | async def get_build(self, mbs_id: int): 45 | client = httpx.AsyncClient() 46 | async with client: 47 | r = await client.get(f"{self.mbs_url}/1/module-builds/{mbs_id}") 48 | 49 | if r.status_code == 404: 50 | raise MBSBuildNotFound("Build not found") 51 | 52 | return r.json() 53 | 54 | async def build(self, token: str, name: str, branch: str, commit: str) -> int: 55 | scmurl = f"https://{settings.gitlab_host}{settings.repo_prefix}/modules/{name}?#{commit}" 56 | 57 | client = httpx.AsyncClient() 58 | async with client: 59 | r = await client.post( 60 | f"{self.mbs_url}/1/module-builds/", 61 | headers={ 62 | "Authorization": f"Bearer {token}" 63 | }, 64 | json={ 65 | "scmurl": scmurl, 66 | "branch": branch, 67 | } 68 | ) 69 | 70 | if r.status_code == 409: 71 | raise MBSConflictException("A MBS conflict occurred") 72 | elif r.status_code == 401: 73 | raise MBSUnauthorizedException("Not authorized") 74 | 75 | data = r.json() 76 | 77 | if not data.get("id"): 78 | raise Exception(data["message"]) 79 | 80 | return data["id"] 81 | -------------------------------------------------------------------------------- /distrobuild/middleware/redis_session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from base64 import b64decode, b64encode 22 | 23 | import typing 24 | import secrets 25 | 26 | import itsdangerous 27 | import json 28 | import aioredis 29 | 30 | from fastapi import FastAPI 31 | from itsdangerous import SignatureExpired, BadTimeSignature 32 | from starlette.datastructures import Secret 33 | from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint, DispatchFunction 34 | from starlette.requests import Request 35 | from starlette.responses import Response 36 | from starlette.types import ASGIApp 37 | 38 | from distrobuild.settings import settings 39 | 40 | 41 | class RedisSessionMiddleware(BaseHTTPMiddleware): 42 | def __init__( 43 | self, 44 | app: ASGIApp, 45 | fapi: FastAPI, 46 | secret_key: typing.Union[str, Secret], 47 | session_cookie: str = "session", 48 | max_age: int = 3000, 49 | same_site: str = "lax", 50 | https_only: bool = False, 51 | dispatch: DispatchFunction = None 52 | ) -> None: 53 | super().__init__(app, dispatch) 54 | self.redis = aioredis.create_redis_pool(settings.redis_url) 55 | self.redis_inited = False 56 | self.app = app 57 | self.signer = itsdangerous.TimestampSigner(str(secret_key)) 58 | self.session_cookie = session_cookie 59 | self.max_age = max_age 60 | self.same_site = same_site 61 | self.https_only = https_only 62 | 63 | @fapi.on_event("shutdown") 64 | async def shutdown(): 65 | self.redis.close() 66 | await self.redis.wait_closed() 67 | 68 | async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: 69 | if not self.redis_inited: 70 | self.redis = await self.redis 71 | self.redis_inited = True 72 | 73 | initial_session_was_empty = True 74 | redis_key = "" 75 | 76 | if self.session_cookie in request.cookies: 77 | data = request.cookies[self.session_cookie].encode("utf-8") 78 | try: 79 | data = self.signer.unsign(data, max_age=self.max_age) 80 | redis_key = data 81 | redis_val = await self.redis.get(data, encoding="utf-8") 82 | request.scope["session"] = json.loads(b64decode(redis_val)) 83 | initial_session_was_empty = False 84 | except (BadTimeSignature, SignatureExpired): 85 | request.scope["session"] = {} 86 | else: 87 | request.scope["session"] = {} 88 | 89 | response = await call_next(request) 90 | 91 | if request.scope["session"]: 92 | redis_val = b64encode(json.dumps(request.scope["session"]).encode("utf-8")) 93 | if redis_key == "": 94 | redis_key = secrets.token_hex(32) 95 | await self.redis.set(redis_key, redis_val) 96 | await self.redis.expire(redis_key, 3000) 97 | else: 98 | await self.redis.set(redis_key, redis_val) 99 | data = self.signer.sign(redis_key) 100 | response.set_cookie(self.session_cookie, data.decode("utf-8"), self.max_age, httponly=True, 101 | samesite=self.same_site, path="/", secure=self.https_only) 102 | elif not initial_session_was_empty: 103 | response.delete_cookie(self.session_cookie, path="/") 104 | await self.redis.delete(redis_key) 105 | 106 | return response 107 | -------------------------------------------------------------------------------- /distrobuild/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from distrobuild.models.enums import ImportStatus, BuildStatus, Repo 22 | from distrobuild.models.package import Package, PackageModule 23 | from distrobuild.models.build import Build, Import, ImportCommit 24 | from distrobuild.models.batch import BatchImport, BatchBuild, BatchImportPackage, BatchBuildPackage 25 | from distrobuild.models.lookaside import LookasideBlob 26 | 27 | -------------------------------------------------------------------------------- /distrobuild/models/batch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from tortoise import Model, fields 22 | from distrobuild.models.build import Import, Build 23 | 24 | 25 | class BatchImport(Model): 26 | id = fields.BigIntField(pk=True) 27 | created_at = fields.DatetimeField(auto_now_add=True) 28 | 29 | imports: fields.ManyToManyRelation[Import] = fields.ManyToManyField("distrobuild.Import", 30 | related_name="batch_imports", 31 | backward_key="batch_import_id", 32 | through="batch_import_packages") 33 | 34 | class Meta: 35 | table = "batch_imports" 36 | 37 | 38 | class BatchBuild(Model): 39 | id = fields.BigIntField(pk=True) 40 | created_at = fields.DatetimeField(auto_now_add=True) 41 | 42 | builds: fields.ManyToManyRelation[Build] = fields.ManyToManyField("distrobuild.Build", 43 | related_name="batch_builds", 44 | backward_key="batch_build_id", 45 | through="batch_build_packages") 46 | 47 | class Meta: 48 | table = "batch_builds" 49 | 50 | 51 | class BatchImportPackage(Model): 52 | id = fields.BigIntField(pk=True) 53 | import_id = fields.BigIntField() 54 | batch_import_id = fields.BigIntField() 55 | 56 | class Meta: 57 | table = "batch_import_packages" 58 | 59 | 60 | class BatchBuildPackage(Model): 61 | id = fields.BigIntField(pk=True) 62 | build_id = fields.BigIntField() 63 | batch_build_id = fields.BigIntField() 64 | 65 | class Meta: 66 | table = "batch_build_packages" 67 | -------------------------------------------------------------------------------- /distrobuild/models/build.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from tortoise import Model, fields 22 | from distrobuild.models.enums import BuildStatus, ImportStatus 23 | 24 | 25 | class Import(Model): 26 | id = fields.BigIntField(pk=True) 27 | created_at = fields.DatetimeField(auto_now_add=True) 28 | updated_at = fields.DatetimeField(auto_add=True, null=True) 29 | package = fields.ForeignKeyField("distrobuild.Package", on_delete="RESTRICT", related_name="imports") 30 | status = fields.CharEnumField(ImportStatus) 31 | module = fields.BooleanField(default=False) 32 | version = fields.IntField() 33 | executor_username = fields.CharField(max_length=255) 34 | 35 | commits: fields.ManyToManyRelation["ImportCommit"] = fields.ManyToManyField("distrobuild.ImportCommit", 36 | related_name="imports", 37 | forward_key="id", 38 | backward_key="import__id", 39 | through="import_commits") 40 | 41 | class Meta: 42 | table = "imports" 43 | ordering = ["-created_at"] 44 | 45 | class PydanticMeta: 46 | exclude = ("batch_imports",) 47 | 48 | 49 | class ImportCommit(Model): 50 | id = fields.BigIntField(pk=True) 51 | commit = fields.CharField(max_length=255) 52 | branch = fields.CharField(max_length=255) 53 | import__id = fields.BigIntField() 54 | 55 | class Meta: 56 | table = "import_commits" 57 | 58 | class PydanticMeta: 59 | exclude = ("import_", "imports", "builds") 60 | 61 | 62 | class Build(Model): 63 | id = fields.BigIntField(pk=True) 64 | created_at = fields.DatetimeField(auto_now_add=True) 65 | updated_at = fields.DatetimeField(auto_add=True, null=True) 66 | package = fields.ForeignKeyField("distrobuild.Package", on_delete="RESTRICT", related_name="builds") 67 | status = fields.CharEnumField(BuildStatus) 68 | mbs = fields.BooleanField(default=False) 69 | signed = fields.BooleanField(default=False) 70 | scratch = fields.BooleanField(default=False) 71 | scratch_merged = fields.BooleanField(default=False) 72 | koji_id = fields.BigIntField(null=True) 73 | mbs_id = fields.BigIntField(null=True) 74 | import_commit = fields.ForeignKeyField("distrobuild.ImportCommit", on_delete="RESTRICT", related_name="builds") 75 | executor_username = fields.CharField(max_length=255) 76 | force_tag = fields.CharField(max_length=255, null=True) 77 | arch_override = fields.TextField(null=True) 78 | exclude_compose = fields.BooleanField(default=False) 79 | point_release = fields.CharField(max_length=255) 80 | 81 | class Meta: 82 | table = "builds" 83 | ordering = ["-created_at"] 84 | 85 | class PydanticMeta: 86 | exclude = ("batch_builds",) 87 | 88 | 89 | class Logs(Model): 90 | id = fields.BigIntField(pk=True) 91 | created_at = fields.DatetimeField(auto_now_add=True) 92 | content = fields.TextField() 93 | 94 | class Meta: 95 | table = "logs" 96 | -------------------------------------------------------------------------------- /distrobuild/models/enums.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from enum import Enum 22 | 23 | 24 | class Repo(str, Enum): 25 | BASEOS = "BASEOS" 26 | APPSTREAM = "APPSTREAM" 27 | POWERTOOLS = "POWERTOOLS" 28 | EXTERNAL = "EXTERNAL" 29 | MODULAR_CANDIDATE = "MODULAR_CANDIDATE" 30 | ORIGINAL = "ORIGINAL" 31 | INFRA = "INFRA" 32 | EXTRAS = "EXTRAS" 33 | 34 | 35 | class BuildStatus(str, Enum): 36 | QUEUED = "QUEUED" 37 | BUILDING = "BUILDING" 38 | FAILED = "FAILED" 39 | SUCCEEDED = "SUCCEEDED" 40 | CANCELLED = "CANCELLED" 41 | 42 | 43 | class ImportStatus(str, Enum): 44 | QUEUED = "QUEUED" 45 | IN_PROGRESS = "IN_PROGRESS" 46 | FAILED = "FAILED" 47 | SUCCEEDED = "SUCCEEDED" 48 | CANCELLED = "CANCELLED" 49 | -------------------------------------------------------------------------------- /distrobuild/models/lookaside.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from tortoise import Model, fields 22 | 23 | 24 | class LookasideBlob(Model): 25 | id = fields.BigIntField(pk=True) 26 | created_at = fields.DatetimeField(auto_now_add=True) 27 | sum = fields.TextField() 28 | executor_username = fields.TextField() 29 | 30 | class Meta: 31 | table = "lookaside_blobs" 32 | -------------------------------------------------------------------------------- /distrobuild/models/package.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | from typing import Optional 21 | 22 | from tortoise import Model, fields 23 | 24 | from distrobuild.models.build import Build, Import 25 | from distrobuild.models.enums import Repo 26 | 27 | 28 | class Package(Model): 29 | id = fields.BigIntField(pk=True) 30 | created_at = fields.DatetimeField(auto_now_add=True) 31 | updated_at = fields.DatetimeField(auto_add=True, null=True) 32 | name = fields.CharField(max_length=255) 33 | responsible_username = fields.CharField(max_length=255) 34 | is_module = fields.BooleanField(default=False) 35 | is_package = fields.BooleanField(default=False) 36 | is_published = fields.BooleanField(default=False) 37 | part_of_module = fields.BooleanField(default=False) 38 | signed = fields.BooleanField(default=False) 39 | last_import = fields.DatetimeField(null=True) 40 | last_build = fields.DatetimeField(null=True) 41 | 42 | el8 = fields.BooleanField(default=False) 43 | el9 = fields.BooleanField(default=False) 44 | repo = fields.CharEnumField(Repo, null=True) 45 | 46 | builds: Optional[fields.ReverseRelation[Build]] 47 | imports: Optional[fields.ReverseRelation[Import]] 48 | 49 | class Meta: 50 | table = "packages" 51 | 52 | class PydanticMeta: 53 | exclude = ("m_module_parent_packages", "m_subpackages") 54 | 55 | 56 | class PackageModule(Model): 57 | id = fields.BigIntField(pk=True) 58 | created_at = fields.DatetimeField(auto_now_add=True) 59 | updated_at = fields.DatetimeField(auto_add=True, null=True) 60 | package = fields.ForeignKeyField("distrobuild.Package", on_delete="RESTRICT", related_name="m_subpackages") 61 | module_parent_package = fields.ForeignKeyField("distrobuild.Package", on_delete="RESTRICT", 62 | related_name="m_module_parent_packages") 63 | 64 | class Meta: 65 | table = "package_modules" 66 | 67 | class PydanticMeta: 68 | backward_relations = False 69 | -------------------------------------------------------------------------------- /distrobuild/routes/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from fastapi import APIRouter 22 | 23 | from distrobuild.routes import builds, imports, packages, bootstrap, oidc, batches, lookaside 24 | 25 | _base_router = APIRouter(prefix="/api") 26 | 27 | 28 | def register_routes(app): 29 | _base_router.include_router(oidc.router) 30 | _base_router.include_router(packages.router) 31 | _base_router.include_router(bootstrap.router) 32 | _base_router.include_router(builds.router) 33 | _base_router.include_router(imports.router) 34 | _base_router.include_router(batches.router) 35 | _base_router.include_router(lookaside.router) 36 | 37 | app.include_router(_base_router) 38 | -------------------------------------------------------------------------------- /distrobuild/routes/batches.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import List, Optional 22 | 23 | from fastapi import APIRouter, Depends, Request, HTTPException 24 | from fastapi_pagination import Page, pagination_params 25 | from fastapi_pagination.ext.tortoise import paginate 26 | from pydantic import BaseModel 27 | 28 | from distrobuild.common import batch_list_check, get_user 29 | from distrobuild.models import BatchImport, BatchBuild, ImportStatus, BuildStatus 30 | from distrobuild.routes.builds import BuildRequest, queue_build 31 | from distrobuild.routes.imports import ImportRequest, import_package_route 32 | from distrobuild.serialize import BatchImport_Pydantic, BatchBuild_Pydantic 33 | from distrobuild_scheduler import merge_scratch_task 34 | 35 | router = APIRouter(prefix="/batches") 36 | 37 | 38 | class BatchImportRequest(BaseModel): 39 | should_precheck: bool = True 40 | allow_stream_branches: bool = False 41 | packages: List[ImportRequest] 42 | 43 | 44 | class BatchBuildRequest(BaseModel): 45 | should_precheck: bool = True 46 | ignore_modules: bool = False 47 | scratch: bool = False 48 | arch_override: Optional[str] 49 | force_tag: Optional[str] 50 | packages: List[BuildRequest] 51 | 52 | 53 | class NewBatchResponse(BaseModel): 54 | id: int 55 | 56 | 57 | @router.get("/imports/", response_model=Page[BatchImport_Pydantic], dependencies=[Depends(pagination_params)]) 58 | async def list_batch_imports(): 59 | return await paginate( 60 | BatchImport.all().prefetch_related( 61 | "imports", "imports__package", "imports__commits").order_by("-created_at")) 62 | 63 | 64 | @router.post("/imports/", response_model=NewBatchResponse) 65 | async def batch_import_package(request: Request, body: BatchImportRequest): 66 | get_user(request) 67 | 68 | if body.should_precheck: 69 | await batch_list_check(body.packages) 70 | 71 | batch = await BatchImport.create() 72 | 73 | for build_request in body.packages: 74 | await import_package_route(request, 75 | dict(**dict(build_request), allow_stream_branches=body.allow_stream_branches), 76 | batch.id) 77 | 78 | return NewBatchResponse(id=batch.id) 79 | 80 | 81 | @router.get("/imports/{batch_import_id}", response_model=BatchImport_Pydantic) 82 | async def get_batch_import(batch_import_id: int): 83 | return await BatchImport_Pydantic.from_queryset_single(BatchImport.filter(id=batch_import_id).first()) 84 | 85 | 86 | @router.post("/imports/{batch_import_id}/cancel", status_code=202) 87 | async def cancel_batch_import(request: Request, batch_import_id: int): 88 | get_user(request) 89 | 90 | batch_import_obj = await BatchImport.filter(id=batch_import_id).prefetch_related("imports").get_or_none() 91 | if not batch_import_obj: 92 | raise HTTPException(404, detail="batch import does not exist") 93 | 94 | for import_ in batch_import_obj.imports: 95 | import_.status = ImportStatus.CANCELLED 96 | await import_.save() 97 | 98 | return {} 99 | 100 | 101 | @router.post("/imports/{batch_import_id}/retry_failed", response_model=NewBatchResponse) 102 | async def retry_failed_batch_imports(request: Request, batch_import_id: int): 103 | get_user(request) 104 | 105 | batch_import_obj = await BatchImport.filter(id=batch_import_id).prefetch_related("imports", 106 | "imports__package").get_or_none() 107 | if not batch_import_obj: 108 | raise HTTPException(404, detail="batch import does not exist") 109 | 110 | packages = [{"package_id": b.package.id} for b in batch_import_obj.imports if 111 | b.status == ImportStatus.CANCELLED or b.status == ImportStatus.FAILED] 112 | 113 | return await batch_import_package(request, BatchImportRequest(packages=packages)) 114 | 115 | 116 | @router.get("/builds/", response_model=Page[BatchBuild_Pydantic], dependencies=[Depends(pagination_params)]) 117 | async def list_batch_builds(): 118 | return await paginate( 119 | BatchBuild.all().prefetch_related( 120 | "builds", "builds__package", "builds__import_commit").order_by( 121 | "-created_at")) 122 | 123 | 124 | @router.post("/builds/", response_model=NewBatchResponse) 125 | async def batch_queue_build(request: Request, body: BatchBuildRequest): 126 | get_user(request) 127 | 128 | if body.should_precheck: 129 | await batch_list_check(body.packages, True) 130 | 131 | batch = await BatchBuild.create() 132 | 133 | for build_request in body.packages: 134 | await queue_build(request, dict(**dict(build_request), ignore_modules=body.ignore_modules, scratch=body.scratch, 135 | arch_override=body.arch_override, force_tag=body.force_tag), batch.id) 136 | 137 | return NewBatchResponse(id=batch.id) 138 | 139 | 140 | @router.get("/builds/{batch_build_id}", response_model=BatchBuild_Pydantic) 141 | async def get_batch_build(batch_build_id: int): 142 | return await BatchBuild_Pydantic.from_queryset_single(BatchBuild.filter(id=batch_build_id).first()) 143 | 144 | 145 | @router.post("/builds/{batch_build_id}/cancel", status_code=202) 146 | async def cancel_batch_build(request: Request, batch_build_id: int): 147 | get_user(request) 148 | 149 | batch_build_obj = await BatchBuild.filter(id=batch_build_id).prefetch_related("builds").get_or_none() 150 | if not batch_build_obj: 151 | raise HTTPException(404, detail="batch build does not exist") 152 | 153 | for build in batch_build_obj.builds: 154 | build.status = BuildStatus.CANCELLED 155 | await build.save() 156 | 157 | return {} 158 | 159 | 160 | @router.post("/builds/{batch_build_id}/merge_scratch", status_code=202) 161 | async def merge_batch_build(request: Request, batch_build_id: int): 162 | get_user(request) 163 | 164 | batch_build_obj = await BatchBuild.filter(id=batch_build_id).prefetch_related("builds").get_or_none() 165 | if not batch_build_obj: 166 | raise HTTPException(404, detail="batch build does not exist") 167 | 168 | for build in batch_build_obj.builds: 169 | if build.scratch and not build.scratch_merged: 170 | await merge_scratch_task(build.id) 171 | 172 | return {} 173 | 174 | 175 | @router.post("/builds/{batch_build_id}/retry_failed", response_model=NewBatchResponse) 176 | async def retry_failed_batch_builds(request: Request, batch_build_id: int): 177 | get_user(request) 178 | 179 | batch_build_obj = await BatchBuild.filter(id=batch_build_id).prefetch_related("builds", 180 | "builds__package").get_or_none() 181 | if not batch_build_obj: 182 | raise HTTPException(404, detail="batch build does not exist") 183 | 184 | packages = [{"package_id": b.package.id} for b in batch_build_obj.builds if 185 | b.status == BuildStatus.CANCELLED or b.status == BuildStatus.FAILED] 186 | 187 | return await batch_queue_build(request, BatchBuildRequest(packages=packages)) 188 | -------------------------------------------------------------------------------- /distrobuild/routes/bootstrap.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | from datetime import datetime 21 | 22 | from fastapi import APIRouter, Request 23 | from fastapi.responses import JSONResponse 24 | from tortoise.transactions import atomic 25 | 26 | from distrobuild.bootstrap import process_repo_dump, process_module_dump 27 | from distrobuild.common import get_user 28 | from distrobuild.models import Repo, Package, Build, Import, ImportStatus, ImportCommit, BuildStatus 29 | from distrobuild.session import koji_session 30 | from distrobuild.settings import settings 31 | 32 | router = APIRouter(prefix="/bootstrap") 33 | 34 | 35 | @atomic() 36 | async def import_build_from_koji(username: str, package: Package, koji_build): 37 | new_import = await Import.create(package_id=package.id, status=ImportStatus.SUCCEEDED, 38 | executor_username=username, version=settings.version) 39 | 40 | commit = koji_build["source"].split("#")[1] 41 | import_commit = await ImportCommit.create(branch=f"{settings.original_import_branch_prefix}{settings.version}", 42 | commit=commit, import__id=new_import.id) 43 | package.last_import = datetime.now() 44 | package.last_build = datetime.now() 45 | 46 | await Build.create(package_id=package.id, status=BuildStatus.SUCCEEDED, 47 | executor_username=username, 48 | point_release=f"{settings.version}_{settings.default_point_release}", 49 | import_commit_id=import_commit.id, koji_id=koji_build["task_id"]) 50 | 51 | await package.save() 52 | 53 | 54 | @router.post("/modules") 55 | async def bootstrap_modules(request: Request): 56 | user = get_user(request) 57 | await process_module_dump(user["preferred_username"]) 58 | return JSONResponse(content={}) 59 | 60 | 61 | @router.post("/import_from_koji", status_code=202) 62 | async def import_from_koji(request: Request): 63 | user = get_user(request) 64 | 65 | all_koji_builds = koji_session.listBuilds() 66 | 67 | packages_without_builds = await Package.filter(last_build__isnull=True).all() 68 | for package in packages_without_builds: 69 | for koji_build in all_koji_builds: 70 | if package.name == koji_build[ 71 | "name"] and not package.is_module and not package.part_of_module and \ 72 | package.repo != Repo.MODULAR_CANDIDATE and koji_build["state"] == 1: 73 | await import_build_from_koji(user["preferred_username"], package, koji_build) 74 | 75 | return {} 76 | 77 | 78 | @router.post("/{repo}") 79 | async def bootstrap_repo(request: Request, repo: Repo): 80 | user = get_user(request) 81 | await process_repo_dump(repo, user["preferred_username"]) 82 | return JSONResponse(content={}) 83 | -------------------------------------------------------------------------------- /distrobuild/routes/builds.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import Optional, Dict 22 | 23 | from fastapi import APIRouter, Depends, HTTPException, Request 24 | from fastapi_pagination import Page, pagination_params 25 | from fastapi_pagination.ext.tortoise import paginate 26 | from pydantic import BaseModel, validator 27 | 28 | from distrobuild.common import gen_body_filters, get_user 29 | from distrobuild.models import Build, Import, ImportCommit, Package, PackageModule, BuildStatus, Repo, BatchBuildPackage 30 | from distrobuild.serialize import Build_Pydantic, BuildGeneral_Pydantic 31 | from distrobuild.session import message_cipher 32 | from distrobuild.settings import settings 33 | from distrobuild_scheduler import build_package_task, merge_scratch_task 34 | 35 | router = APIRouter(prefix="/builds") 36 | 37 | 38 | class BuildRequest(BaseModel): 39 | scratch: bool = False 40 | ignore_modules: bool = False 41 | arch_override: Optional[str] 42 | force_tag: Optional[str] 43 | only_branch: Optional[str] 44 | package_id: Optional[int] 45 | package_name: Optional[str] 46 | 47 | @validator("package_name") 48 | def validate(cls, package_name, values): 49 | if (not values.get("package_id") and not package_name) or (values.get("package_id") and package_name): 50 | raise ValueError("either package_id or package_name is required") 51 | return package_name 52 | 53 | 54 | @router.get("/", response_model=Page[BuildGeneral_Pydantic], dependencies=[Depends(pagination_params)]) 55 | async def list_builds(): 56 | return await paginate(Build.all().order_by("-created_at").prefetch_related("package", "import_commit")) 57 | 58 | 59 | @router.get("/{build_id}", response_model=Build_Pydantic) 60 | async def get_build(build_id: int): 61 | return await Build_Pydantic.from_queryset_single( 62 | Build.filter(id=build_id).prefetch_related("package", "import_commit").first() 63 | ) 64 | 65 | 66 | @router.post("/{build_id}/cancel", status_code=202) 67 | async def cancel_build(request: Request, build_id: int): 68 | get_user(request) 69 | 70 | build_obj = await Build.filter(id=build_id, cancelled=False).get_or_none() 71 | if not build_obj: 72 | raise HTTPException(404, detail="build does not exist or is already cancelled") 73 | 74 | build_obj.status = BuildStatus.CANCELLED 75 | await build_obj.save() 76 | 77 | return {} 78 | 79 | 80 | @router.post("/{build_id}/merge_scratch", status_code=202) 81 | async def merge_build(request: Request, build_id: int): 82 | get_user(request) 83 | 84 | build_obj = await Build.filter(id=build_id, scratch_merged=False, scratch=True).get_or_none() 85 | if not build_obj: 86 | raise HTTPException(404, detail="build does not exist, is already merged or not a scratch build") 87 | 88 | await merge_scratch_task(build_obj.id) 89 | 90 | return {} 91 | 92 | 93 | @router.post("/", status_code=202) 94 | async def queue_build(request: Request, body: Dict[str, BuildRequest], batch_build_id: Optional[int] = None): 95 | user = get_user(request) 96 | 97 | filters = gen_body_filters(body) 98 | package = await Package.filter(**filters).get_or_none() 99 | if not package: 100 | raise HTTPException(404, detail="package does not exist") 101 | 102 | if package.repo == Repo.MODULAR_CANDIDATE: 103 | raise HTTPException(400, detail="modular subpackages cannot be built, build the main module") 104 | 105 | extras = {} 106 | token = None 107 | 108 | if body.get("force_tag"): 109 | extras["force_tag"] = body.get("force_tag") 110 | 111 | if body.get("scratch"): 112 | extras["scratch"] = True 113 | 114 | latest_build = await Build.filter(package_id=package.id, status=BuildStatus.SUCCEEDED).prefetch_related( 115 | "import_commit").order_by( 116 | "-created_at").first() 117 | if not latest_build: 118 | return {} 119 | import_commits = [latest_build.import_commit] 120 | else: 121 | filters = { 122 | "package_id": package.id 123 | } 124 | if body.get("ignore_modules"): 125 | filters["module"] = False 126 | 127 | latest_import = await Import.filter(**filters).order_by("-created_at").first() 128 | import_commits = await ImportCommit.filter(import__id=latest_import.id).all() 129 | 130 | if body.get("arch_override"): 131 | extras["arch_override"] = body.get("arch_override") 132 | 133 | only_branch = body.get("only_branch") 134 | for import_commit in import_commits: 135 | if "-beta" not in import_commit.branch: 136 | if only_branch and import_commit.branch != only_branch: 137 | continue 138 | 139 | stream_branch_prefix = f"{settings.original_import_branch_prefix}{settings.version}-stream" 140 | if import_commit.branch.startswith(stream_branch_prefix): 141 | if body.get("ignore_modules"): 142 | continue 143 | if package.part_of_module and not package.is_module: 144 | continue 145 | extras["mbs"] = True 146 | if not token: 147 | token = request.session.get("token") 148 | 149 | # temporarily skip containeronly streams 150 | containeronly_stream_prefix = f"{settings.original_import_branch_prefix}{settings.version}-containeronly-stream" 151 | if import_commit.branch.startswith(containeronly_stream_prefix): 152 | continue 153 | 154 | build = await Build.create(package_id=package.id, status=BuildStatus.QUEUED, 155 | executor_username=user["preferred_username"], 156 | point_release=f"{settings.version}_{settings.default_point_release}", 157 | import_commit_id=import_commit.id, **extras) 158 | if batch_build_id: 159 | await BatchBuildPackage.create(build_id=build.id, batch_build_id=batch_build_id) 160 | await build_package_task(package.id, build.id, token) 161 | 162 | if only_branch: 163 | break 164 | 165 | return {} 166 | -------------------------------------------------------------------------------- /distrobuild/routes/imports.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import Optional, Dict 22 | 23 | from fastapi import APIRouter, Depends, HTTPException, Request 24 | from fastapi_pagination import pagination_params, Page 25 | from fastapi_pagination.ext.tortoise import paginate 26 | from pydantic import BaseModel, validator 27 | from starlette.responses import PlainTextResponse 28 | 29 | from distrobuild.common import gen_body_filters, create_import_order, get_user 30 | from distrobuild.models import Import, Package, Repo, ImportStatus 31 | from distrobuild.serialize import Import_Pydantic, ImportGeneral_Pydantic 32 | from distrobuild.settings import settings 33 | from distrobuild_scheduler import import_package_task 34 | 35 | router = APIRouter(prefix="/imports") 36 | 37 | 38 | class ImportRequest(BaseModel): 39 | full_history: bool = False 40 | allow_stream_branches: bool = False 41 | single_tag: Optional[str] 42 | package_id: Optional[int] 43 | package_name: Optional[str] 44 | 45 | @validator("package_name") 46 | def validate(cls, package_name, values): 47 | if (not values.get("package_id") and not package_name) or (values.get("package_id") and package_name): 48 | raise ValueError("either package_id or package_name is required") 49 | return package_name 50 | 51 | 52 | @router.get("/", response_model=Page[ImportGeneral_Pydantic], dependencies=[Depends(pagination_params)]) 53 | async def list_imports(): 54 | return await paginate(Import.all().order_by("-created_at").prefetch_related("package", "commits")) 55 | 56 | 57 | @router.get("/{import_id}", response_model=Import_Pydantic) 58 | async def get_import(import_id: int): 59 | return await Import_Pydantic.from_queryset_single(Import.filter(id=import_id).prefetch_related("package").first()) 60 | 61 | 62 | @router.get("/{import_id}/logs", response_class=PlainTextResponse) 63 | async def get_import_logs(import_id: int): 64 | import_obj = await Import.filter(id=import_id).get_or_none() 65 | if not import_obj: 66 | raise HTTPException(404, detail="import does not exist") 67 | try: 68 | with open(f"{settings.import_logs_dir}/import-{import_obj.id}.log") as f: 69 | return f.read() 70 | except FileNotFoundError: 71 | raise HTTPException(412, detail="import not started or log has expired") 72 | 73 | 74 | @router.post("/{import_id}/cancel", status_code=202) 75 | async def cancel_import(request: Request, import_id: int): 76 | get_user(request) 77 | 78 | import_obj = await Import.filter(id=import_id, cancelled=False).get_or_none() 79 | if not import_obj: 80 | raise HTTPException(404, detail="import does not exist or is already cancelled") 81 | 82 | import_obj.status = ImportStatus.CANCELLED 83 | await import_obj.save() 84 | 85 | return {} 86 | 87 | 88 | @router.post("/", status_code=202) 89 | async def import_package_route(request: Request, body: Dict[str, ImportRequest], batch_import_id: Optional[int] = None): 90 | user = get_user(request) 91 | 92 | filters = gen_body_filters(body) 93 | package = await Package.filter(**filters).get_or_none() 94 | if not package: 95 | raise HTTPException(404, detail="package does not exist") 96 | 97 | if package.repo == Repo.MODULAR_CANDIDATE: 98 | raise HTTPException(401, detail="modular subpackages cannot be imported") 99 | 100 | build_order = await create_import_order(package, user["preferred_username"], batch_import_id) 101 | await import_package_task(build_order[0][0], build_order[0][1], build_order[1:], 102 | body.get("allow_stream_branches") or False) 103 | 104 | return {} 105 | -------------------------------------------------------------------------------- /distrobuild/routes/lookaside.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import hashlib 21 | 22 | from fastapi import APIRouter, Request, File, UploadFile 23 | from pydantic import BaseModel 24 | from tortoise.transactions import atomic 25 | 26 | from distrobuild.common import get_user 27 | from distrobuild.models import LookasideBlob 28 | from distrobuild.session import lookaside_session 29 | 30 | router = APIRouter(prefix="/lookaside") 31 | 32 | 33 | class LookasideUploadResponse(BaseModel): 34 | sha256sum: str 35 | 36 | 37 | @atomic() 38 | @router.post("/", response_model=LookasideUploadResponse) 39 | async def put_file_in_lookaside(request: Request, file: UploadFile = File(...)): 40 | user = get_user(request) 41 | 42 | file_content = await file.read() 43 | await file.seek(0) 44 | 45 | sha256sum = hashlib.sha256(file_content).hexdigest() 46 | lookaside_session.upload(file.file, sha256sum) 47 | 48 | await LookasideBlob.create(sum=sha256sum, executor_username=user["preferred_username"]) 49 | 50 | return LookasideUploadResponse(sha256sum=sha256sum) 51 | -------------------------------------------------------------------------------- /distrobuild/routes/oidc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from fastapi import APIRouter, Request 22 | from fastapi.responses import RedirectResponse 23 | 24 | from distrobuild.auth import oidc 25 | from distrobuild.session import message_cipher 26 | from distrobuild.settings import settings 27 | 28 | router = APIRouter(prefix="/oidc") 29 | 30 | 31 | @router.get("/start_flow") 32 | async def start_flow(request: Request): 33 | redirect_uri = request.url_for("callback") 34 | return await oidc.authorize_redirect(request, redirect_uri) 35 | 36 | 37 | @router.get("/logout") 38 | async def start_flow(request: Request): 39 | request.session.clear() 40 | return RedirectResponse(url="/") 41 | 42 | 43 | @router.get("/callback") 44 | async def callback(request: Request): 45 | token = await oidc.authorize_access_token(request) 46 | user = await oidc.parse_id_token(request, token) 47 | 48 | if settings.oidc_required_group: 49 | groups = user.get("groups") 50 | if not groups: 51 | request.session.update(not_authorized="No 'groups' attribute in ID token") 52 | return RedirectResponse(url="/") 53 | else: 54 | if settings.oidc_required_group not in groups: 55 | request.session.update(not_authorized=f"User not in '{settings.oidc_required_group}' group") 56 | return RedirectResponse(url="/") 57 | 58 | access_token = message_cipher.encrypt(token["access_token"].encode()).decode() 59 | request.session.update(user=user, token=access_token) 60 | return RedirectResponse(url="/") 61 | -------------------------------------------------------------------------------- /distrobuild/routes/packages.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import Optional 22 | 23 | from fastapi import APIRouter, Depends, HTTPException, Request 24 | from fastapi_pagination import Page, pagination_params 25 | from fastapi_pagination.ext.tortoise import paginate 26 | 27 | from distrobuild.common import get_user 28 | from distrobuild.models import Package, Repo, Build, BuildStatus 29 | from distrobuild.serialize import Package_Pydantic, PackageGeneral_Pydantic 30 | from distrobuild.session import koji_session 31 | 32 | router = APIRouter(prefix="/packages") 33 | 34 | 35 | @router.get("/", response_model=Page[PackageGeneral_Pydantic], dependencies=[Depends(pagination_params)]) 36 | async def list_packages(name: Optional[str] = None, modules_only: bool = False, non_modules_only: bool = False, 37 | exclude_modular_candidates: bool = False, no_builds_only: bool = False, 38 | with_builds_only: bool = False, no_imports_only: bool = False, with_imports_only: bool = False, 39 | exclude_part_of_modules: bool = False): 40 | filters = {} 41 | if name: 42 | filters["name__icontains"] = name 43 | if modules_only: 44 | filters["is_module"] = True 45 | if non_modules_only: 46 | filters["is_package"] = True 47 | if exclude_modular_candidates: 48 | filters["repo__not"] = Repo.MODULAR_CANDIDATE 49 | if no_builds_only: 50 | filters["last_build__isnull"] = True 51 | if with_builds_only: 52 | filters["last_build__not_isnull"] = True 53 | if no_imports_only: 54 | filters["last_import__isnull"] = True 55 | if with_imports_only: 56 | filters["last_import__not_isnull"] = True 57 | if exclude_part_of_modules: 58 | filters["part_of_module"] = False 59 | 60 | return await paginate(Package.all().order_by("updated_at", "name").filter(**filters)) 61 | 62 | 63 | @router.get("/{package_id}", response_model=Package_Pydantic) 64 | async def get_package(package_id: int): 65 | return await Package_Pydantic.from_queryset_single( 66 | Package.filter(id=package_id).prefetch_related("builds", "imports").first()) 67 | 68 | 69 | @router.post("/{package_id}/reset_latest_build") 70 | async def reset_latest_build_for_package(request: Request, package_id: int): 71 | user = get_user(request) 72 | 73 | latest_build = await Build.filter(package_id=package_id, status=BuildStatus.SUCCEEDED).order_by( 74 | "-created_at").first() 75 | if not latest_build: 76 | raise HTTPException(412, detail="no successful build found") 77 | 78 | build_tasks = koji_session.listBuilds(taskID=latest_build.koji_id) 79 | if len(build_tasks) == 0: 80 | raise HTTPException(412, detail="no build tasks found for latest build") 81 | 82 | koji_session.resetBuild(build_tasks[0]["build_id"]) 83 | 84 | latest_build.status = BuildStatus.CANCELLED 85 | latest_build.executor_username = user["preferred_username"] 86 | await latest_build.save() 87 | 88 | return {} 89 | -------------------------------------------------------------------------------- /distrobuild/serialize/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from tortoise.contrib.pydantic import pydantic_model_creator 22 | 23 | from distrobuild import models 24 | 25 | Package_Pydantic = pydantic_model_creator(models.Package, name="Package") 26 | PackageGeneral_Pydantic = pydantic_model_creator(models.Package, name="PackageGeneral", exclude=("imports", "builds")) 27 | PackageModule_Pydantic = pydantic_model_creator(models.PackageModule, name="PackageModule") 28 | Build_Pydantic = pydantic_model_creator(models.Build, name="Build", exclude=("import_commit.import_",)) 29 | BuildGeneral_Pydantic = pydantic_model_creator(models.Build, name="BuildGeneral", 30 | exclude=("import_commit.import_", "package.imports", "package.builds")) 31 | Import_Pydantic = pydantic_model_creator(models.Import, name="Import", exclude=("commits.builds",)) 32 | ImportGeneral_Pydantic = pydantic_model_creator(models.Import, name="ImportGeneral", 33 | exclude=("package.imports", "package.builds")) 34 | BatchImport_Pydantic = pydantic_model_creator(models.BatchImport, name="BatchImport", 35 | exclude=("imports.package.imports", "imports.package.builds")) 36 | BatchBuild_Pydantic = pydantic_model_creator(models.BatchBuild, name="BatchBuild", 37 | exclude=("builds.package.imports", "builds.package.builds")) 38 | -------------------------------------------------------------------------------- /distrobuild/session.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import gitlab 22 | import koji 23 | from cryptography.fernet import Fernet 24 | from koji_cli.lib import activate_session 25 | 26 | from distrobuild.lookaside import Lookaside 27 | from distrobuild.mbs import MBSClient 28 | from distrobuild.settings import settings 29 | 30 | gl = gitlab.Gitlab(f"https://{settings.gitlab_host}", private_token=settings.gitlab_api_key) 31 | 32 | koji_config = koji.read_config("koji") 33 | koji_session = koji.ClientSession(koji_config["server"], koji_config) 34 | activate_session(koji_session, koji_config) 35 | mbs_client = MBSClient(settings.mbs_url) 36 | message_cipher = Fernet(settings.message_secret) 37 | lookaside_session = Lookaside(settings.storage_addr) 38 | -------------------------------------------------------------------------------- /distrobuild/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import Optional, List 22 | from pydantic import BaseSettings 23 | 24 | 25 | class Settings(BaseSettings): 26 | bugs_api_key: str 27 | gitlab_api_key: str 28 | session_secret: str 29 | message_secret: str 30 | tag_prefix: str = "rocky" 31 | active_point_releases: List[str] = ["3", "4"] 32 | default_point_release: str = "3" 33 | redis_url: str = "redis://localhost" 34 | database_url: str = "postgres://postgres:postgres@localhost/dbuild" 35 | production: bool = False 36 | debug: bool = False 37 | 38 | # srpmproc 39 | gitlab_host: str 40 | repo_prefix: str = "/" 41 | storage_addr: str 42 | ssh_user: str = "git" 43 | ssh_port: int = 22 44 | ssh_key_location: Optional[str] 45 | version: int = 8 46 | no_storage_download: bool = False 47 | no_storage_upload: bool = False 48 | import_logs_dir: str = "/tmp" 49 | original_import_branch_prefix: str = "r" 50 | original_rpm_prefix: str = "https://git.rockylinux.org/staging/src" 51 | original_module_prefix: str = "https://git.rockylinux.org/original/modules" 52 | 53 | # mbs 54 | mbs_url: str 55 | 56 | # sigul 57 | disable_sigul: bool = False 58 | sigul_config_file: str = "/etc/distrobuild/sigul.conf" 59 | sigul_passphrase: str 60 | sigul_key_name: str = "signing" 61 | sigul_key_id: str 62 | 63 | # oidc 64 | oidc_issuer: str 65 | oidc_client_id: str 66 | oidc_client_secret: str 67 | oidc_scopes: str = "https://id.fedoraproject.org/scope/groups https://mbs.rockylinux.org/oidc/mbs-submit-build" 68 | oidc_required_group: Optional[str] 69 | 70 | # appearance 71 | distribution: str = "Rocky Linux" 72 | 73 | # scheduler options 74 | broker_url: str 75 | routing_key: str = "distrobuild" 76 | workers: int = 10 77 | 78 | class Config: 79 | env_file = "/etc/distrobuild/settings" 80 | 81 | 82 | settings = Settings() 83 | if settings.default_point_release not in settings.active_point_releases: 84 | raise Exception("Default point release not active.") 85 | 86 | TORTOISE_ORM = { 87 | "connections": {"default": settings.database_url}, 88 | "apps": { 89 | "distrobuild": { 90 | "models": [ 91 | "aerich.models", 92 | "distrobuild.models", 93 | ], 94 | "default_connection": "default", 95 | }, 96 | }, 97 | } 98 | -------------------------------------------------------------------------------- /distrobuild/srpmproc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import asyncio 22 | import json 23 | 24 | from typing import Optional 25 | 26 | from distrobuild.settings import settings 27 | 28 | 29 | async def import_project(import_id: int, source_rpm: str, module_mode: bool = False, 30 | single_tag: Optional[str] = None, original: bool = False, 31 | allow_stream_branches: bool = False) -> dict: 32 | upstream_prefix = f"ssh://{settings.ssh_user}@{settings.gitlab_host}:{settings.ssh_port}{settings.repo_prefix}" 33 | 34 | prefix_no_trailing = settings.repo_prefix 35 | if prefix_no_trailing.endswith("/"): 36 | prefix_no_trailing = prefix_no_trailing[:-1] 37 | 38 | upstream_prefix_https = f"{settings.gitlab_host}{prefix_no_trailing}" 39 | 40 | args = [ 41 | "--source-rpm", 42 | source_rpm, 43 | "--storage-addr", 44 | settings.storage_addr, 45 | "--version", 46 | str(settings.version), 47 | "--upstream-prefix", 48 | upstream_prefix, 49 | "--ssh-user", 50 | settings.ssh_user, 51 | ] 52 | 53 | if settings.ssh_key_location: 54 | args.append("--ssh-key-location") 55 | args.append(settings.ssh_key_location) 56 | 57 | if settings.no_storage_download: 58 | args.append("--no-storage-download") 59 | 60 | if settings.no_storage_upload: 61 | args.append("--no-storage-upload") 62 | 63 | if module_mode: 64 | args.append("--module-mode") 65 | 66 | if single_tag: 67 | args.append("--single-tag") 68 | args.append(single_tag) 69 | 70 | if original: 71 | args.append("--import-branch-prefix") 72 | args.append(settings.original_import_branch_prefix) 73 | args.append("--rpm-prefix") 74 | args.append(settings.original_rpm_prefix) 75 | args.append("--module-prefix") 76 | args.append(settings.original_module_prefix) 77 | 78 | if allow_stream_branches: 79 | args.append("--branch-suffix s") 80 | else: 81 | args.append("--strict-branch-mode") 82 | 83 | args.append("--rpm-prefix") 84 | args.append("https://git.rockylinux.org/staging/src-rhel/rpms") 85 | args.append("--module-prefix") 86 | args.append("https://git.rockylinux.org/staging/src-rhel/modules") 87 | 88 | f = open(f"{settings.import_logs_dir}/import-{import_id}.log", "w") 89 | 90 | proc = await asyncio.create_subprocess_exec("srpmproc", *args, stdout=asyncio.subprocess.PIPE, stderr=f) 91 | 92 | last_line = "" 93 | while True: 94 | line = (await proc.stdout.readline()).decode("utf-8") 95 | f.write(line) 96 | 97 | if proc.stdout.at_eof(): 98 | break 99 | 100 | last_line = line 101 | 102 | await proc.wait() 103 | f.close() 104 | 105 | if proc.returncode != 0: 106 | raise Exception("srpmproc failed") 107 | else: 108 | return json.loads(last_line.strip()) 109 | -------------------------------------------------------------------------------- /distrobuild/templates/not_authorized.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Distrobuild - Not authorized 9 | 10 | 15 | 16 | 17 | 18 |
19 | Rocky Linux logo
21 |

You're not authorized to access this application

22 | {{ message }}

23 | 24 | Go back 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /distrobuild_scheduler/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import json 22 | import logging 23 | 24 | from typing import Tuple, Optional, List 25 | 26 | import aio_pika 27 | 28 | from distrobuild.settings import settings 29 | 30 | # singleton 31 | connection: Optional[aio_pika.RobustConnection] = None 32 | channel: Optional[aio_pika.Channel] = None 33 | logger = logging.getLogger("distrobuild_scheduler") 34 | logging.basicConfig() 35 | 36 | if settings.debug: 37 | logger.setLevel(logging.DEBUG) 38 | else: 39 | logger.setLevel(logging.INFO) 40 | 41 | 42 | async def init_channel(loop) -> None: 43 | global channel 44 | global connection 45 | connection = await aio_pika.connect_robust(settings.broker_url, loop=loop) 46 | logger.info("[*] Connected to amqp") 47 | channel = await connection.channel() 48 | 49 | 50 | async def import_package_task(package_id: int, import_id: int, dependents: List[Tuple[int, int]], 51 | allow_stream_branches: bool = False): 52 | msg_body = { 53 | "message": "import_package", 54 | "package_id": package_id, 55 | "import_id": import_id, 56 | "allow_stream_branches": allow_stream_branches, 57 | "dependents": dependents, 58 | } 59 | encoded = json.dumps(msg_body).encode() 60 | 61 | await channel.default_exchange.publish( 62 | aio_pika.Message( 63 | body=encoded, 64 | ), 65 | routing_key=settings.routing_key, 66 | ) 67 | 68 | 69 | async def build_package_task(package_id: int, build_id: int, token: Optional[str]): 70 | msg_body = { 71 | "message": "build_package", 72 | "package_id": package_id, 73 | "build_id": build_id, 74 | "token": token, 75 | } 76 | encoded = json.dumps(msg_body).encode() 77 | 78 | await channel.default_exchange.publish( 79 | aio_pika.Message( 80 | body=encoded, 81 | ), 82 | routing_key=settings.routing_key, 83 | ) 84 | 85 | 86 | async def merge_scratch_task(build_id: int): 87 | msg_body = { 88 | "message": "merge_scratch", 89 | "build_id": build_id, 90 | } 91 | encoded = json.dumps(msg_body).encode() 92 | 93 | await channel.default_exchange.publish( 94 | aio_pika.Message( 95 | body=encoded, 96 | ), 97 | routing_key=settings.routing_key, 98 | ) 99 | -------------------------------------------------------------------------------- /distrobuild_scheduler/build_package.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from typing import Optional 22 | 23 | import koji 24 | from tortoise.transactions import atomic 25 | 26 | from distrobuild.common import tags 27 | from distrobuild.mbs import MBSConflictException 28 | from distrobuild.models import Build, BuildStatus, Package, Repo 29 | from distrobuild.session import koji_session, mbs_client 30 | from distrobuild.settings import settings 31 | from distrobuild_scheduler import logger 32 | 33 | from distrobuild_scheduler.utils import gitlabify 34 | 35 | 36 | @atomic() 37 | async def do(package: Package, build: Build, token: Optional[str]): 38 | try: 39 | if build.mbs: 40 | task_id = await mbs_client.build(token, package.name, build.import_commit.branch, 41 | build.import_commit.commit) 42 | 43 | build.mbs_id = task_id 44 | build.status = BuildStatus.BUILDING 45 | await build.save() 46 | else: 47 | default_target = tags.base() 48 | if package.repo == Repo.EXTRAS: 49 | default_target = tags.extras() 50 | 51 | target = default_target if not build.force_tag else build.force_tag 52 | 53 | host = f"git+https://{settings.gitlab_host}{settings.repo_prefix}" 54 | source = f"{host}/rpms/{gitlabify(package.name)}.git?#{build.import_commit.commit}" 55 | 56 | opts = {} 57 | if build.scratch: 58 | opts["scratch"] = True 59 | if build.arch_override: 60 | opts["arch_override"] = koji.parse_arches(build.arch_override) 61 | 62 | task_id = koji_session.build(source, target, opts) 63 | 64 | build.koji_id = task_id 65 | build.status = BuildStatus.BUILDING 66 | await build.save() 67 | except MBSConflictException: 68 | build.status = BuildStatus.CANCELLED 69 | await build.save() 70 | except Exception: 71 | raise 72 | 73 | 74 | # noinspection DuplicatedCode 75 | async def task(package_id: int, build_id: int, token: Optional[str]): 76 | build = await Build.filter(id=build_id).prefetch_related("import_commit").get() 77 | 78 | if build.status == BuildStatus.CANCELLED: 79 | return 80 | 81 | package = await Package.filter(id=package_id).get() 82 | try: 83 | await do(package, build, token) 84 | except Exception as e: 85 | logger.error(e) 86 | build.status = BuildStatus.FAILED 87 | package.last_build = None 88 | finally: 89 | await build.save() 90 | await package.save() 91 | -------------------------------------------------------------------------------- /distrobuild_scheduler/import_package.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import datetime 22 | 23 | from typing import List, Tuple 24 | 25 | from tortoise.transactions import atomic 26 | 27 | from distrobuild.common import tags 28 | from distrobuild.models import ImportStatus, Package, Import, ImportCommit, Repo 29 | from distrobuild.session import koji_session, gl 30 | from distrobuild.settings import settings 31 | from distrobuild import srpmproc 32 | from distrobuild_scheduler import logger 33 | 34 | from distrobuild_scheduler.utils import gitlabify 35 | 36 | 37 | @atomic() 38 | async def do(package: Package, package_import: Import, allow_stream_branches: bool): 39 | koji_session.packageListAdd(tags.base(), package.name, "distrobuild") 40 | 41 | original = package.repo == Repo.ORIGINAL 42 | branch_commits_and_versions = await srpmproc.import_project(package_import.id, package.name, package_import.module, 43 | original=original, allow_stream_branches=allow_stream_branches) 44 | 45 | branch_commits = branch_commits_and_versions['branch_commits'] 46 | for branch in branch_commits.keys(): 47 | commit = branch_commits[branch] 48 | await ImportCommit.create(branch=branch, commit=commit, import__id=package_import.id) 49 | 50 | package.last_import = datetime.datetime.now() 51 | 52 | mode = "modules" if package_import.module else "rpms" 53 | project = gl.projects.get(f"{settings.repo_prefix[1:]}/{mode}/{gitlabify(package.name)}") 54 | project.visibility = "public" 55 | project.save() 56 | 57 | 58 | # noinspection DuplicatedCode 59 | async def task(package_id: int, import_id: int, dependents: List[Tuple[int, int]], allow_stream_branches: bool = False): 60 | package = await Package.filter(id=package_id).get() 61 | package_import = await Import.filter(id=import_id).get() 62 | 63 | if package_import.status != ImportStatus.CANCELLED: 64 | try: 65 | package_import.status = ImportStatus.IN_PROGRESS 66 | await package_import.save() 67 | 68 | await do(package, package_import, allow_stream_branches) 69 | except Exception as e: 70 | logger.error(e) 71 | package_import.status = ImportStatus.FAILED 72 | package.last_import = None 73 | else: 74 | package_import.status = ImportStatus.SUCCEEDED 75 | finally: 76 | await package_import.save() 77 | await package.save() 78 | 79 | if len(dependents) > 0: 80 | await task(dependents[0][0], dependents[0][1], dependents[1:]) 81 | -------------------------------------------------------------------------------- /distrobuild_scheduler/main.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import asyncio 22 | import json 23 | 24 | from tortoise import Tortoise 25 | 26 | from distrobuild_scheduler.sigul import check_sigul_key 27 | 28 | Tortoise.init_models(["distrobuild.models"], "distrobuild") 29 | 30 | from distrobuild.settings import TORTOISE_ORM, settings 31 | 32 | from distrobuild.session import message_cipher 33 | 34 | # noinspection PyUnresolvedReferences 35 | from distrobuild_scheduler import init_channel, build_package, import_package, logger, periodic_tasks, merge_scratch 36 | 37 | 38 | async def consume_messages(i: int): 39 | from distrobuild_scheduler import channel 40 | 41 | queue = await channel.declare_queue(settings.routing_key, auto_delete=False) 42 | async with queue.iterator() as queue_iter: 43 | logger.info(f"[*] Waiting for messages (worker {i})") 44 | async for message in queue_iter: 45 | async with message.process(): 46 | body = json.loads(message.body.decode()) 47 | msg = body.get("message") 48 | 49 | if msg == "import_package": 50 | await import_package.task(body["package_id"], body["import_id"], body["dependents"], 51 | body["allow_stream_branches"]) 52 | elif msg == "build_package": 53 | token = body.get("token") 54 | if token: 55 | token = message_cipher.decrypt(token.encode()).decode() 56 | await build_package.task(body["package_id"], body["build_id"], token) 57 | elif msg == "merge_scratch": 58 | await merge_scratch.task(body["build_id"]) 59 | else: 60 | logger.error("[*] Received unknown message") 61 | 62 | 63 | def schedule_periodic_tasks(): 64 | asyncio.create_task(periodic_tasks.check_build_status()) 65 | asyncio.create_task(periodic_tasks.sign_unsigned_builds()) 66 | 67 | 68 | async def main(loop): 69 | try: 70 | await Tortoise.init(config=TORTOISE_ORM) 71 | await init_channel(loop) 72 | 73 | if not settings.disable_sigul: 74 | await check_sigul_key() 75 | 76 | schedule_periodic_tasks() 77 | 78 | tasks = [consume_messages(i) for i in range(0, settings.workers)] 79 | await asyncio.wait(tasks) 80 | finally: 81 | from distrobuild_scheduler import connection 82 | logger.info("[*] Shutting down") 83 | await connection.close() 84 | -------------------------------------------------------------------------------- /distrobuild_scheduler/merge_scratch.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | from tortoise.transactions import atomic 22 | 23 | from distrobuild.models import Build, BuildStatus 24 | from distrobuild.session import koji_session 25 | 26 | 27 | @atomic() 28 | async def do(build: Build): 29 | if build.koji_id and build.scratch and not build.scratch_merged and build.status == BuildStatus.SUCCEEDED: 30 | koji_session.mergeScratch(build.koji_id) 31 | build.scratch_merged = True 32 | await build.save() 33 | 34 | 35 | # noinspection DuplicatedCode 36 | async def task(build_id: int): 37 | build = await Build.filter(id=build_id, scratch=True).get() 38 | 39 | if not build: 40 | return 41 | 42 | if build.status == BuildStatus.CANCELLED: 43 | return 44 | 45 | try: 46 | await do(build) 47 | except Exception as e: 48 | print(e) 49 | -------------------------------------------------------------------------------- /distrobuild_scheduler/periodic_tasks.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import asyncio 22 | import datetime 23 | import xmlrpc 24 | 25 | import koji 26 | from tortoise.transactions import atomic 27 | 28 | from distrobuild.common import tags 29 | from distrobuild.models import Build, BuildStatus, Package, PackageModule, Repo 30 | from distrobuild.session import koji_session, mbs_client 31 | from distrobuild.settings import settings 32 | 33 | from distrobuild_scheduler import logger 34 | from distrobuild_scheduler.sigul import sign_koji_package 35 | 36 | 37 | async def sign_build_rpms(build_rpms): 38 | for build_rpm in build_rpms: 39 | rpm_sigs = koji_session.queryRPMSigs(build_rpm["id"]) 40 | for rpm_sig in rpm_sigs: 41 | if rpm_sig["sigkey"] == settings.sigul_key_id: 42 | continue 43 | 44 | nvr_arch = "%s.%s" % (build_rpm["nvr"], build_rpm["arch"]) 45 | logger.debug(f"[*] signing NVR {nvr_arch}") 46 | await sign_koji_package(nvr_arch) 47 | koji_session.writeSignedRPM(nvr_arch, settings.sigul_key_id) 48 | 49 | 50 | def tag_if_not_tagged(build_history, nvr, tag): 51 | if "tag_listing" in build_history: 52 | for t in build_history["tag_listing"]: 53 | if t["tag.name"] == tag: 54 | return 55 | 56 | koji_session.tagBuild(tag, nvr) 57 | 58 | 59 | async def sign_mbs_build(build: Build, mbs_build): 60 | tasks = mbs_build.get("tasks") 61 | if not tasks: 62 | return 63 | rpms = tasks.get("rpms") 64 | if not rpms: 65 | return 66 | for rpm_name in rpms.keys(): 67 | if rpm_name == "module-build-macros": 68 | continue 69 | rpm = rpms[rpm_name] 70 | if rpm["state"] != 1: 71 | continue 72 | 73 | build_rpms = koji_session.listBuildRPMs(rpm["nvr"]) 74 | if len(build_rpms) > 0: 75 | await sign_build_rpms(build_rpms) 76 | 77 | package_modules = await PackageModule.filter( 78 | module_parent_package_id=build.package.id).prefetch_related( 79 | "package").all() 80 | for package_module in package_modules: 81 | package_module_package = await Package.filter(id=package_module.package.id).first() 82 | package_module_package.last_build = datetime.datetime.now() 83 | await package_module_package.save() 84 | 85 | name = mbs_build["name"] 86 | name_devel = f"{name}-devel" 87 | koji_session.packageListAdd(tags.module_compose(), name, "distrobuild") 88 | koji_session.packageListAdd(tags.module_compose(), name_devel, "distrobuild") 89 | koji_tag = mbs_build["koji_tag"] 90 | context = mbs_build["context"] 91 | nvr = koji_tag.replace("module-", "").replace(f"-{context}", f".{context}") 92 | nvr_devel = nvr.replace(name, name_devel) 93 | koji_session.tagBuild(tags.module_compose(), nvr) 94 | koji_session.tagBuild(tags.module_compose(), nvr_devel) 95 | 96 | build.signed = True 97 | await build.save() 98 | 99 | 100 | @atomic() 101 | async def atomic_sign_unsigned_builds(build: Build): 102 | if build.koji_id: 103 | koji_session.packageListAdd(tags.compose(), build.package.name, "distrobuild") 104 | 105 | build_tasks = koji_session.listBuilds(taskID=build.koji_id) 106 | for build_task in build_tasks: 107 | build_history = koji_session.queryHistory(build=build_task["build_id"]) 108 | tag_if_not_tagged(build_history, build_task["nvr"], tags.compose()) 109 | 110 | build_rpms = koji_session.listBuildRPMs(build_task["build_id"]) 111 | await sign_build_rpms(build_rpms) 112 | 113 | build.signed = True 114 | await build.save() 115 | elif build.mbs_id: 116 | mbs_build = await mbs_client.get_build(build.mbs_id) 117 | await sign_mbs_build(build, mbs_build) 118 | 119 | siblings = mbs_build.get("siblings") 120 | if siblings: 121 | for sibling in siblings: 122 | n_mbs_build = await mbs_client.get_build(sibling) 123 | await sign_mbs_build(build, n_mbs_build) 124 | 125 | 126 | @atomic() 127 | async def atomic_check_build_status(build: Build): 128 | if build.koji_id: 129 | task_info = koji_session.getTaskInfo(build.koji_id, request=True) 130 | if task_info["state"] == koji.TASK_STATES["CLOSED"]: 131 | build.status = BuildStatus.SUCCEEDED 132 | await build.save() 133 | 134 | package = await Package.filter(id=build.package_id).get() 135 | package.last_build = datetime.datetime.now() 136 | await package.save() 137 | elif task_info["state"] == koji.TASK_STATES["CANCELED"]: 138 | build.status = BuildStatus.CANCELLED 139 | await build.save() 140 | elif task_info["state"] == koji.TASK_STATES["FAILED"]: 141 | try: 142 | task_result = koji_session.getTaskResult(build.koji_id) 143 | logger.debug(task_result) 144 | except (koji.BuildError, xmlrpc.client.Fault): 145 | build.status = BuildStatus.FAILED 146 | except koji.GenericError: 147 | build.status = BuildStatus.CANCELLED 148 | finally: 149 | await build.save() 150 | elif build.mbs_id: 151 | build_info = await mbs_client.get_build(build.mbs_id) 152 | state = build_info["state_name"] 153 | if state == "ready": 154 | build.status = BuildStatus.SUCCEEDED 155 | await build.save() 156 | 157 | package = await Package.filter(id=build.package_id).get() 158 | package.last_build = datetime.datetime.now() 159 | await package.save() 160 | elif state == "failed": 161 | build.status = BuildStatus.FAILED 162 | await build.save() 163 | 164 | 165 | async def check_build_status(): 166 | while True: 167 | try: 168 | logger.debug("[*] Running periodic task: check_build_status") 169 | 170 | builds = await Build.filter(status=BuildStatus.BUILDING).all() 171 | for build in builds: 172 | try: 173 | await atomic_check_build_status(build) 174 | except Exception as e: 175 | logger.error(f"check_build_status: {e}") 176 | 177 | # run every 5 minutes 178 | await asyncio.sleep(60 * 5) 179 | except Exception as e: 180 | logger.error(f"check_build_status wrapper: {e}") 181 | 182 | 183 | async def sign_unsigned_builds(): 184 | if not settings.disable_sigul: 185 | try: 186 | while True: 187 | logger.debug("[*] Running periodic task: sign_unsigned_builds") 188 | 189 | builds = await Build.filter(signed=False, status=BuildStatus.SUCCEEDED).prefetch_related( 190 | "package").all() 191 | for build in builds: 192 | try: 193 | await atomic_sign_unsigned_builds(build) 194 | except Exception as e: 195 | logger.error(f"sign_unsigned_builds: {e}") 196 | 197 | # run every 5 minutes 198 | await asyncio.sleep(60 * 5) 199 | except Exception as e: 200 | logger.error(f"sign_unsigned_builds wrapper: {e}") 201 | -------------------------------------------------------------------------------- /distrobuild_scheduler/sigul.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | import asyncio 22 | 23 | from typing import List 24 | 25 | from distrobuild.settings import settings 26 | from distrobuild_scheduler import logger 27 | 28 | 29 | class SigulException(Exception): 30 | pass 31 | 32 | 33 | async def run_sigul(args: List[str]): 34 | args.insert(0, "-c") 35 | args.insert(1, settings.sigul_config_file) 36 | args.insert(2, "--batch") 37 | 38 | proc = await asyncio.create_subprocess_exec("sigul", 39 | *args, 40 | stdin=asyncio.subprocess.PIPE, 41 | stdout=asyncio.subprocess.PIPE, 42 | stderr=asyncio.subprocess.PIPE) 43 | proc.stdin.write(f"{settings.sigul_passphrase}\0".encode()) 44 | await proc.wait() 45 | 46 | if proc.returncode != 0: 47 | err = (await proc.stderr.read()).decode() 48 | # means that the package is already signed. 49 | # stupid error 50 | if "ERROR: I/O error: EOFError()" not in err: 51 | logger.debug('sigul returned with code: %s', proc.returncode) 52 | raise SigulException(err) 53 | 54 | return await proc.communicate() 55 | 56 | 57 | async def check_sigul_key(): 58 | await run_sigul(["get-public-key", settings.sigul_key_name]) 59 | 60 | 61 | async def sign_koji_package(nvr_arch: str): 62 | await run_sigul([ 63 | "sign-rpm", 64 | "--koji-only", 65 | "--store-in-koji", 66 | "--v3-signature", 67 | settings.sigul_key_name, 68 | nvr_arch 69 | ]) 70 | -------------------------------------------------------------------------------- /distrobuild_scheduler/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | 21 | def gitlabify(name: str) -> str: 22 | if name == "tree": 23 | return "treepkg" 24 | return name.replace("+", "plus") 25 | -------------------------------------------------------------------------------- /migrations/distrobuild/0_20210226090638_init.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | CREATE TABLE IF NOT EXISTS "aerich" ( 25 | "id" SERIAL NOT NULL PRIMARY KEY, 26 | "version" VARCHAR(255) NOT NULL, 27 | "app" VARCHAR(20) NOT NULL, 28 | "content" JSONB NOT NULL 29 | ); 30 | -------------------------------------------------------------------------------- /migrations/distrobuild/1_20210303223722_packages.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | CREATE TABLE IF NOT EXISTS "packages" ( 25 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 26 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 | "updated_at" TIMESTAMPTZ, 28 | "name" VARCHAR(255) NOT NULL, 29 | "responsible_username" VARCHAR(255) NOT NULL, 30 | "is_module" BOOL NOT NULL DEFAULT False, 31 | "is_package" BOOL NOT NULL DEFAULT False, 32 | "part_of_module" BOOL NOT NULL DEFAULT False, 33 | "last_import" TIMESTAMPTZ, 34 | "last_build" TIMESTAMPTZ, 35 | "el8" BOOL NOT NULL DEFAULT False, 36 | "el9" BOOL NOT NULL DEFAULT False, 37 | "repo" VARCHAR(17) 38 | ); 39 | COMMENT ON COLUMN "packages"."repo" IS 'BASEOS: BASEOS\nAPPSTREAM: APPSTREAM\nPOWERTOOLS: POWERTOOLS\nEXTERNAL: EXTERNAL\nMODULAR_CANDIDATE: MODULAR_CANDIDATE\nORIGINAL: ORIGINAL\nINFRA: INFRA';; 40 | CREATE TABLE IF NOT EXISTS "package_modules" ( 41 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 42 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 43 | "updated_at" TIMESTAMPTZ, 44 | "package_id" BIGINT NOT NULL REFERENCES "packages" ("id") ON DELETE RESTRICT, 45 | "module_parent_package_id" BIGINT NOT NULL REFERENCES "packages" ("id") ON DELETE RESTRICT 46 | );; 47 | -- downgrade -- 48 | DROP TABLE IF EXISTS "package_modules"; 49 | DROP TABLE IF EXISTS "packages"; 50 | -------------------------------------------------------------------------------- /migrations/distrobuild/2_20210303223853_builds.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | CREATE TABLE IF NOT EXISTS "imports" 25 | ( 26 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 27 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 28 | "updated_at" TIMESTAMPTZ, 29 | "status" VARCHAR(11) NOT NULL, 30 | "module" BOOL NOT NULL DEFAULT False, 31 | "version" INT NOT NULL, 32 | "executor_username" VARCHAR(255) NOT NULL, 33 | "package_id" BIGINT NOT NULL REFERENCES "packages" ("id") ON DELETE RESTRICT 34 | ); 35 | COMMENT ON COLUMN "imports"."status" IS 'QUEUED: QUEUED\nIN_PROGRESS: IN_PROGRESS\nFAILED: FAILED\nSUCCEEDED: SUCCEEDED\nCANCELLED: CANCELLED';; 36 | CREATE TABLE IF NOT EXISTS "import_commits" 37 | ( 38 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 39 | "commit" VARCHAR(255) NOT NULL, 40 | "branch" VARCHAR(255) NOT NULL, 41 | "import__id" BIGINT NOT NULL REFERENCES "imports" ("id") ON DELETE RESTRICT 42 | );; 43 | CREATE TABLE IF NOT EXISTS "builds" 44 | ( 45 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 46 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, 47 | "updated_at" TIMESTAMPTZ, 48 | "status" VARCHAR(9) NOT NULL, 49 | "mbs" BOOL NOT NULL DEFAULT False, 50 | "koji_id" BIGINT, 51 | "mbs_id" BIGINT, 52 | "executor_username" VARCHAR(255) NOT NULL, 53 | "force_tag" VARCHAR(255), 54 | "exclude_compose" BOOL NOT NULL DEFAULT False, 55 | "point_release" VARCHAR(255) NOT NULL, 56 | "import_commit_id" BIGINT NOT NULL REFERENCES "import_commits" ("id") ON DELETE RESTRICT, 57 | "package_id" BIGINT NOT NULL REFERENCES "packages" ("id") ON DELETE RESTRICT 58 | ); 59 | COMMENT ON COLUMN "builds"."status" IS 'QUEUED: QUEUED\nBUILDING: BUILDING\nFAILED: FAILED\nSUCCEEDED: SUCCEEDED\nCANCELLED: CANCELLED';; 60 | -- downgrade -- 61 | DROP TABLE IF EXISTS "builds"; 62 | DROP TABLE IF EXISTS "import_commits"; 63 | DROP TABLE IF EXISTS "imports"; 64 | -------------------------------------------------------------------------------- /migrations/distrobuild/3_20210409144010_batch.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | CREATE TABLE IF NOT EXISTS "batch_imports" 25 | ( 26 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 27 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP 28 | ); 29 | CREATE TABLE IF NOT EXISTS "batch_import_packages" 30 | ( 31 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 32 | "import_id" BIGINT NOT NULL REFERENCES "imports" ("id") ON DELETE RESTRICT, 33 | "batch_import_id" BIGINT NOT NULL REFERENCES "batch_imports" ("id") ON DELETE RESTRICT 34 | ); 35 | CREATE TABLE IF NOT EXISTS "batch_builds" 36 | ( 37 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 38 | "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP 39 | ); 40 | CREATE TABLE IF NOT EXISTS "batch_build_packages" 41 | ( 42 | "id" BIGSERIAL NOT NULL PRIMARY KEY, 43 | "build_id" BIGINT NOT NULL REFERENCES "builds" ("id") ON DELETE RESTRICT, 44 | "batch_build_id" BIGINT NOT NULL REFERENCES "batch_builds" ("id") ON DELETE RESTRICT 45 | ); 46 | -- downgrade -- 47 | DROP TABLE IF EXISTS "batch_import_packages"; 48 | DROP TABLE IF EXISTS "batches_imports"; 49 | DROP TABLE IF EXISTS "batch_build_packages"; 50 | DROP TABLE IF EXISTS "batches_builds"; 51 | -------------------------------------------------------------------------------- /migrations/distrobuild/4_20210410103510_sign.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | alter table packages 25 | add column signed bool default false not null; 26 | alter table packages 27 | add column is_published bool default false not null; 28 | alter table builds 29 | add column signed bool default false not null; 30 | 31 | -- downgrade -- 32 | alter table packages 33 | drop column signed; 34 | alter table packages 35 | drop column is_published; 36 | alter table builds 37 | drop column signed; 38 | -------------------------------------------------------------------------------- /migrations/distrobuild/5_20210410192210_lookaside.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | create table if not exists lookaside_blobs 25 | ( 26 | id bigserial not null primary key, 27 | created_at timestamptz not null default current_timestamp, 28 | sum text not null, 29 | executor_username text not null 30 | ); 31 | 32 | -- downgrade -- 33 | drop table lookaside_blobs; -------------------------------------------------------------------------------- /migrations/distrobuild/6_20210411154910_cancel.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | alter table imports add column cancelled bool default false not null; 25 | alter table builds add column cancelled bool default false not null; 26 | 27 | -- downgrade -- 28 | alter table imports drop column cancelled; 29 | alter table builds drop column cancelled; 30 | -------------------------------------------------------------------------------- /migrations/distrobuild/6_20210417213810_scratch.sql: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | -- upgrade -- 24 | alter table builds 25 | add column scratch bool default false not null; 26 | alter table builds 27 | add column scratch_merged bool default false not null; 28 | alter table builds 29 | add column arch_override text; 30 | 31 | -- downgrade -- 32 | alter table builds 33 | drop column scratch; 34 | alter table builds 35 | drop column scratch_merged; 36 | alter table builds 37 | drop column arch_override; 38 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.65.1 2 | tortoise-orm[asyncpg]==0.17.3 3 | aerich==0.5.3 4 | starlette-context==0.3.2 5 | aiofiles==0.7.0 6 | jinja2==3.0.1 7 | fastapi-pagination[tortoise]==0.7.3 8 | koji==1.33.0 9 | pydantic[dotenv]==1.8.2 10 | python-dotenv==0.17.1 11 | python-gitlab==2.7.1 12 | aio-pika==6.8.0 13 | python-multipart==0.0.5 14 | authlib==0.15.3 15 | itsdangerous==2.0.1 16 | httpx==0.18.1 17 | cryptography==3.4.7 18 | boto3==1.17.80 19 | google-cloud-storage==1.38.0 20 | aioredis==1.3.1 21 | -------------------------------------------------------------------------------- /run_scheduler.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021 The Distrobuild Authors 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all 11 | # copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | # SOFTWARE. 20 | import asyncio 21 | 22 | from tortoise import Tortoise 23 | 24 | from distrobuild_scheduler.main import main 25 | 26 | if __name__ == "__main__": 27 | loop = asyncio.new_event_loop() 28 | try: 29 | loop.run_until_complete(main(loop)) 30 | finally: 31 | loop.run_until_complete(Tortoise.close_connections()) 32 | loop.close() 33 | -------------------------------------------------------------------------------- /ui/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /ui/babel.config.js: -------------------------------------------------------------------------------- 1 | const prod = process.env.NODE_ENV === 'production'; 2 | const dev = !prod; 3 | 4 | module.exports = { 5 | presets: [ 6 | '@babel/preset-react', 7 | '@babel/preset-typescript', 8 | '@babel/preset-env', 9 | ], 10 | plugins: [ 11 | '@babel/plugin-transform-runtime', 12 | dev && 'react-refresh/babel', 13 | ].filter(Boolean), 14 | }; 15 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "distrobuild-ui", 3 | "version": "1.0.0", 4 | "description": "Distrobuild frontend", 5 | "main": "index.js", 6 | "author": "Mustafa Gezen ", 7 | "license": "BSD-2", 8 | "private": true, 9 | "scripts": { 10 | "start": "NODE_ENV=development webpack serve", 11 | "build": "NODE_ENV=production webpack" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.13.15", 15 | "@babel/plugin-transform-runtime": "^7.13.15", 16 | "@babel/preset-env": "^7.13.15", 17 | "@babel/preset-react": "^7.13.13", 18 | "@babel/preset-typescript": "^7.13.0", 19 | "@carbon/icons-react": "^10.24.0", 20 | "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3", 21 | "@types/axios": "^0.14.0", 22 | "@types/carbon-components": "^10.27.0", 23 | "@types/carbon-components-react": "^7.26.0", 24 | "@types/carbon__icons-react": "^10.24.0", 25 | "@types/react": "^17.0.0", 26 | "@types/react-dom": "^17.0.0", 27 | "@types/react-router-dom": "^5.1.7", 28 | "autoprefixer": "^10.2.5", 29 | "await-to-js": "^2.1.1", 30 | "axios": "^0.21.1", 31 | "babel-loader": "^8.2.2", 32 | "carbon-components": "^10.27.0", 33 | "carbon-components-react": "^7.27.0", 34 | "carbon-icons": "^7.0.7", 35 | "clean-webpack-plugin": "^3.0.0", 36 | "compression-webpack-plugin": "^7.1.2", 37 | "css-loader": "^5.0.1", 38 | "html-webpack-plugin": "^4.5.1", 39 | "postcss": "^8.2.9", 40 | "postcss-loader": "^5.2.0", 41 | "prettier": "^2.2.1", 42 | "react": "^17.0.1", 43 | "react-dom": "^17.0.1", 44 | "react-refresh": "^0.10.0", 45 | "react-router-dom": "^5.2.0", 46 | "style-loader": "^2.0.0", 47 | "tailwindcss": "^2.1.1", 48 | "webpack": "^5.19.0", 49 | "webpack-cli": "^4.6.0", 50 | "webpack-dev-server": "^3.11.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Distrobuild 6 | 7 | 8 |
9 | 10 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ui/src/api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import axios from 'axios'; 24 | 25 | export const Axios = axios.create({ 26 | baseURL: '/api', 27 | withCredentials: true, 28 | }); 29 | 30 | export interface IPaginated { 31 | items: T[]; 32 | total: number; 33 | page: number; 34 | size: number; 35 | } 36 | 37 | export interface IPackage { 38 | id: string; 39 | name: string; 40 | el8: boolean; 41 | is_module: boolean; 42 | is_package: boolean; 43 | is_published: boolean; 44 | signed: boolean; 45 | part_of_module: boolean; 46 | last_import: string; 47 | repo: string; 48 | builds: IBuild[]; 49 | imports: IImport[]; 50 | } 51 | 52 | export interface IImport { 53 | id: string; 54 | created_at: string; 55 | executor_username: string; 56 | status: string; 57 | module: boolean; 58 | commits: Commit[]; 59 | package: IPackage; 60 | } 61 | 62 | export interface IBuild { 63 | id: string; 64 | created_at: string; 65 | executor_username: string; 66 | status: string; 67 | koji_id: string; 68 | mbs_id: string; 69 | mbs: boolean; 70 | branch: string; 71 | commit: string; 72 | import_commit: Commit; 73 | package: IPackage; 74 | scratch: boolean; 75 | scratch_merged: boolean; 76 | } 77 | 78 | export interface IBatchBuild { 79 | id: string; 80 | created_at: string; 81 | builds: IBuild[]; 82 | } 83 | 84 | export interface IBatchImport { 85 | id: string; 86 | created_at: string; 87 | imports: IImport[]; 88 | } 89 | 90 | export interface Commit { 91 | id: number; 92 | commit: string; 93 | branch: string; 94 | import__id: number; 95 | } 96 | -------------------------------------------------------------------------------- /ui/src/components/BatchShow.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React, { useState } from 'react'; 24 | import { useParams } from 'react-router-dom'; 25 | import { 26 | Button, 27 | DataTableSkeleton, 28 | Modal, 29 | Tile, 30 | } from 'carbon-components-react'; 31 | import { BuildsTable } from './BuildsTable'; 32 | import { Axios } from '../api'; 33 | import to from 'await-to-js'; 34 | import { ImportsTable } from './ImportsTable'; 35 | 36 | export interface BatchShowParams { 37 | id: string; 38 | } 39 | 40 | export interface BatchShowProps { 41 | name: string; 42 | } 43 | 44 | export const BatchShow = (props: BatchShowProps) => { 45 | const [showCancelModal, setShowCancelModal] = React.useState(false); 46 | const [showSuccessModal, setShowSuccessModal] = React.useState(false); 47 | const [showRetryModal, setShowRetryModal] = React.useState(false); 48 | const [showScratchModal, setShowScratchModal] = React.useState(false); 49 | const [disable, setDisable] = React.useState(false); 50 | 51 | const params = useParams(); 52 | const [res, setRes] = useState(undefined); 53 | 54 | React.useEffect(() => { 55 | (async () => { 56 | const [, res] = await to( 57 | Axios.get(`/batches/${props.name}/${params.id}`) 58 | ); 59 | if (!res) { 60 | setRes(null); 61 | return; 62 | } 63 | 64 | setRes(res.data); 65 | })().then(); 66 | }, []); 67 | 68 | const cancelBatch = () => { 69 | setDisable(true); 70 | 71 | (async () => { 72 | const [err] = await to( 73 | Axios.post(`/batches/${props.name}/${params.id}/cancel`) 74 | ); 75 | if (err) { 76 | alert('API Error'); 77 | setDisable(false); 78 | return; 79 | } 80 | 81 | setShowCancelModal(false); 82 | setDisable(false); 83 | setShowSuccessModal(true); 84 | })().then(); 85 | }; 86 | 87 | const retryFailed = () => { 88 | setDisable(true); 89 | 90 | (async () => { 91 | const [err, res] = await to( 92 | Axios.post(`/batches/${props.name}/${params.id}/retry_failed`) 93 | ); 94 | if (err) { 95 | alert('API Error'); 96 | setDisable(false); 97 | return; 98 | } 99 | 100 | setShowCancelModal(false); 101 | setDisable(false); 102 | window.location.pathname = `/batches/${props.name}/${res.data.id}`; 103 | })().then(); 104 | }; 105 | 106 | const mergeScratch = () => { 107 | setDisable(true); 108 | 109 | (async () => { 110 | const [err] = await to( 111 | Axios.post(`/batches/${props.name}/${params.id}/merge_scratch`) 112 | ); 113 | if (err) { 114 | alert('API Error'); 115 | setDisable(false); 116 | return; 117 | } 118 | 119 | setShowScratchModal(false); 120 | setDisable(false); 121 | window.location.reload(); 122 | })().then(); 123 | }; 124 | 125 | const failedItems = 126 | res && 127 | res[props.name] 128 | .map((x) => { 129 | if (x.status !== 'FAILED' && x.status !== 'CANCELLED') { 130 | return false; 131 | } 132 | 133 | x[props.name] = res; 134 | return x; 135 | }) 136 | .filter(Boolean); 137 | 138 | const succeededItems = 139 | res && 140 | res[props.name] 141 | .map((x) => { 142 | if (x.status !== 'SUCCEEDED') { 143 | return false; 144 | } 145 | 146 | x[props.name] = res; 147 | return x; 148 | }) 149 | .filter(Boolean); 150 | 151 | const shouldShowCancel = 152 | res && res[props.name].length > failedItems.length + succeededItems.length; 153 | 154 | return ( 155 |
156 | setShowCancelModal(false)} 162 | onRequestSubmit={() => cancelBatch()} 163 | > 164 | Are you sure you want to cancel this batch? 165 | 166 | setShowSuccessModal(false)} 172 | onRequestSubmit={() => { 173 | window.location.href = '/'; 174 | }} 175 | > 176 | Successfully cancelled batch 177 | 178 | setShowRetryModal(false)} 184 | onRequestSubmit={() => retryFailed()} 185 | > 186 | Are you sure you want to retry failed {props.name}? 187 | 188 | {props.name === 'builds' && ( 189 | setShowScratchModal(false)} 195 | onRequestSubmit={() => mergeScratch()} 196 | > 197 | Are you sure you want merge all scratch builds? 198 | 199 | )} 200 | 201 | 202 |

Batch #{params.id}

203 |
204 | {window.STATE.authenticated && 205 | !shouldShowCancel && 206 | failedItems && 207 | failedItems.length > 0 && ( 208 | 211 | )} 212 | {window.STATE.authenticated && 213 | !shouldShowCancel && 214 | succeededItems && 215 | succeededItems.filter((x) => x.scratch).length !== 216 | succeededItems.filter((x) => x.scratch_merged).length && ( 217 | 220 | )} 221 | {window.STATE.authenticated && shouldShowCancel && ( 222 | 228 | )} 229 |
230 |
231 | 232 | {res === null && ( 233 |
234 | Could not get package 235 |
236 | )} 237 | 238 | {(res || res === undefined) && ( 239 | <> 240 | {res && ( 241 |
242 |
252 |
253 | {Math.round( 254 | ((failedItems.length + succeededItems.length) / 255 | res[props.name].length) * 256 | 100 257 | )} 258 | % 259 |
260 |
261 | )} 262 |
263 |
264 |
Failed {props.name}
265 | {res && ( 266 | <> 267 | {props.name === 'builds' && ( 268 | 276 | )} 277 | {props.name === 'imports' && ( 278 | 286 | )} 287 | 288 | )} 289 | {res === undefined && ( 290 | 291 | )} 292 |
293 |
294 |
Succeeded {props.name}
295 | {res && ( 296 | <> 297 | {props.name === 'builds' && ( 298 | 306 | )} 307 | {props.name === 'imports' && ( 308 | 316 | )} 317 | 318 | )} 319 | {res === undefined && ( 320 | 321 | )} 322 |
323 |
324 | 325 | )} 326 |
327 | ); 328 | }; 329 | -------------------------------------------------------------------------------- /ui/src/components/BuildBatchShow.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { BatchShow } from './BatchShow'; 25 | 26 | export const BuildBatchShow = () => { 27 | return ; 28 | }; 29 | -------------------------------------------------------------------------------- /ui/src/components/BuildBatches.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BatchList } from './BatchList'; 3 | 4 | export const BuildBatches = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /ui/src/components/BuildsList.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { PaginatedResourceList } from './PaginatedResourceList'; 25 | 26 | export const BuildsList = () => { 27 | return ; 28 | }; 29 | -------------------------------------------------------------------------------- /ui/src/components/BuildsTable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { 25 | DataTableSkeleton, 26 | Table, 27 | TableBody, 28 | TableCell, 29 | TableHead, 30 | TableHeader, 31 | TableRow, 32 | } from 'carbon-components-react'; 33 | import { IBuild, IPaginated } from '../api'; 34 | import { statusToTag } from '../utils/tags'; 35 | import { Link } from 'react-router-dom'; 36 | import { commitsToLinks } from '../utils/commits'; 37 | 38 | export interface BuildsTableProps { 39 | builds: IPaginated; 40 | } 41 | 42 | export const buildHeaders = ['ID', 'Initiated', 'Package', 'Status', 'Executor', '']; 43 | 44 | export const BuildsTable = (props: BuildsTableProps) => { 45 | const { builds } = props; 46 | return ( 47 | <> 48 | {!builds && ( 49 | 53 | )} 54 | {builds && ( 55 | 56 | 57 | 58 | {buildHeaders.map((header) => ( 59 | {header} 60 | ))} 61 | 62 | 63 | 64 | {builds.items.map((item: IBuild) => ( 65 | 66 | {item.id} 67 | 68 | {new Date(item.created_at).toLocaleString()} 69 | 70 | 71 | 72 | {item.package.name} 73 | 74 | 75 | 76 | {statusToTag( 77 | item.status, 78 | item.scratch ? item.scratch_merged : undefined 79 | )} 80 | 81 | 82 | 86 | { 87 | item.executor_username 88 | } 89 | 90 | 91 | 92 | {item.koji_id && ( 93 | 97 | Koji 98 | 99 | )} 100 | {item.mbs_id && ( 101 | 105 | MBS 106 | 107 | )} 108 | {commitsToLinks(item.mbs, item.package, [item.import_commit])} 109 | {!item.mbs_id && 110 | !item.koji_id && 111 | item.status === 'FAILED' && ( 112 | Failed during invocation 113 | )} 114 | 115 | 116 | ))} 117 | 118 |
119 | )} 120 | 121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /ui/src/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import to from 'await-to-js'; 25 | 26 | import { IPaginated, Axios, IImport, IBuild } from '../api'; 27 | import { BuildsTable } from './BuildsTable'; 28 | import { ImportsTable } from './ImportsTable'; 29 | 30 | export const Dashboard = () => { 31 | const [imports, setImports] = React.useState>(); 32 | const [builds, setBuilds] = React.useState>(); 33 | 34 | React.useEffect(() => { 35 | (async () => { 36 | const [, res] = await to(Axios.get('/imports/?size=5')); 37 | if (res) { 38 | setImports(res.data); 39 | } 40 | })().then(); 41 | (async () => { 42 | const [, res] = await to(Axios.get('/builds/?size=5')); 43 | if (res) { 44 | setBuilds(res.data); 45 | } 46 | })().then(); 47 | }, []); 48 | 49 | return ( 50 | <> 51 |
52 |

Latest builds

53 | 54 |
55 |
56 |

Latest imports

57 | 58 |
59 | 60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /ui/src/components/ImportBatchShow.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { BatchShow } from './BatchShow'; 25 | 26 | export const ImportBatchShow = () => { 27 | return ; 28 | }; 29 | -------------------------------------------------------------------------------- /ui/src/components/ImportBatches.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BatchList } from './BatchList'; 3 | 4 | export const ImportBatches = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /ui/src/components/ImportsList.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { PaginatedResourceList } from './PaginatedResourceList'; 25 | 26 | export const ImportsList = () => { 27 | return ; 28 | }; 29 | -------------------------------------------------------------------------------- /ui/src/components/ImportsTable.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { IImport, IPaginated } from '../api'; 25 | import { 26 | DataTableSkeleton, 27 | Table, 28 | TableBody, 29 | TableCell, 30 | TableHead, 31 | TableHeader, 32 | TableRow, 33 | } from 'carbon-components-react'; 34 | import { statusToTag } from '../utils/tags'; 35 | import { Link } from 'react-router-dom'; 36 | import { commitsToLinks } from '../utils/commits'; 37 | 38 | export interface ImportsTableProps { 39 | imports: IPaginated; 40 | } 41 | 42 | const importHeaders = ['ID', 'Initiated', 'Package', 'Status', 'Executor', '']; 43 | 44 | export const ImportsTable = (props: ImportsTableProps) => { 45 | const { imports } = props; 46 | 47 | return ( 48 | <> 49 | {!imports && ( 50 | 54 | )} 55 | {imports && ( 56 | 57 | 58 | 59 | {importHeaders.map((header) => ( 60 | {header} 61 | ))} 62 | 63 | 64 | 65 | {imports.items.map((item: IImport) => ( 66 | 67 | {item.id} 68 | 69 | {new Date(item.created_at).toLocaleString()} 70 | 71 | 72 | 73 | {item.package.name} 74 | 75 | 76 | {statusToTag(item.status)} 77 | 78 | 82 | { 83 | item.executor_username 84 | } 85 | 86 | 87 | 88 | 89 | Logs 90 | 91 | {commitsToLinks(item.module, item.package, item.commits)} 92 | 93 | 94 | ))} 95 | 96 |
97 | )} 98 | 99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /ui/src/components/Lookaside.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { FileUploaderDropContainer } from 'carbon-components-react'; 25 | import { Axios } from '../api'; 26 | 27 | export const Lookaside = () => { 28 | const [disabled, setDisabled] = React.useState(false); 29 | const [sum, setSum] = React.useState(undefined); 30 | 31 | const onAddFile = (_, { addedFiles }) => { 32 | setDisabled(true); 33 | 34 | const formData = new FormData(); 35 | formData.append('file', addedFiles[0]); 36 | 37 | Axios.post('/lookaside/', formData, { 38 | headers: { 39 | 'Content-Type': 'multipart/form-data', 40 | }, 41 | }) 42 | .then((res) => { 43 | setSum(res.data.sha256sum); 44 | setDisabled(false); 45 | }) 46 | .catch((_) => { 47 | alert('Could not upload file'); 48 | setDisabled(false); 49 | }); 50 | }; 51 | 52 | return ( 53 |
54 |

Lookaside

55 | 56 | {sum && ( 57 |
58 | Uploaded file has sum: {sum} 59 |
60 | )} 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /ui/src/components/PaginatedResourceList.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | /* 24 | * Copyright (c) 2021 The Distrobuild Authors 25 | * 26 | * Permission is hereby granted, free of charge, to any person obtaining a copy 27 | * of this software and associated documentation files (the "Software"), to deal 28 | * in the Software without restriction, including without limitation the rights 29 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | * copies of the Software, and to permit persons to whom the Software is 31 | * furnished to do so, subject to the following conditions: 32 | * 33 | * The above copyright notice and this permission notice shall be included in all 34 | * copies or substantial portions of the Software. 35 | * 36 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 42 | * SOFTWARE. 43 | */ 44 | 45 | import React from 'react'; 46 | import { changeQueryParam, getQueryParam, PageChangeEvent } from '../misc'; 47 | import { Axios, IPaginated } from '../api'; 48 | import to from 'await-to-js'; 49 | import { 50 | DataTable, 51 | DataTableRow, 52 | DataTableSkeleton, 53 | Pagination, 54 | Table, 55 | TableBody, 56 | TableCell, 57 | TableContainer, 58 | TableHead, 59 | TableHeader, 60 | TableRow, 61 | } from 'carbon-components-react'; 62 | import { Link } from 'react-router-dom'; 63 | import { statusToTag } from '../utils/tags'; 64 | import { commitsToLinks } from '../utils/commits'; 65 | 66 | export interface PaginatedResourceListProps { 67 | name: string; 68 | } 69 | 70 | export const PaginatedResourceList = ( 71 | props: PaginatedResourceListProps 72 | ) => { 73 | const [size, setSize] = React.useState( 74 | Math.min(100, Number(getQueryParam('size') || 25)) 75 | ); 76 | const [page, setPage] = React.useState(Number(getQueryParam('page') || 1)); 77 | 78 | const [paginatedRes, setPaginatedRes] = React.useState({} as IPaginated); 79 | 80 | React.useEffect(() => { 81 | (async () => { 82 | const [, res] = await to( 83 | Axios.get(`/${props.name}/`, { 84 | params: { 85 | page: Math.max(0, page - 1), 86 | size, 87 | }, 88 | }) 89 | ); 90 | if (res) { 91 | res.data.items = res.data.items.map((item) => { 92 | item.id = item.id.toString(); 93 | return item; 94 | }); 95 | setPaginatedRes(res.data); 96 | } 97 | })().then(); 98 | }, [page, size]); 99 | 100 | const onPageChange = (e: PageChangeEvent) => { 101 | setPage(e.page); 102 | setSize(e.pageSize); 103 | window.scrollTo(0, 0); 104 | changeQueryParam('size', e.pageSize.toString()); 105 | changeQueryParam('page', e.page.toString()); 106 | }; 107 | 108 | const headers = [ 109 | { header: 'ID', key: 'id' }, 110 | { header: 'Initiated', key: 'created_at' }, 111 | { header: 'Package', key: 'package' }, 112 | { header: 'Status', key: 'status' }, 113 | { header: 'Executor', key: 'executor_username' }, 114 | { header: '', key: 'extra' }, 115 | ]; 116 | 117 | return paginatedRes.items ? ( 118 | 119 | {({ 120 | rows, 121 | headers, 122 | getHeaderProps, 123 | getRowProps, 124 | getTableProps, 125 | getTableContainerProps, 126 | }) => ( 127 | 133 | 134 | 135 | 136 | {headers.map((header, i) => ( 137 | 138 | {header.header} 139 | 140 | ))} 141 | 142 | 143 | 144 | {rows.map((row, i) => { 145 | const pkg: T | undefined = 146 | paginatedRes.items[ 147 | paginatedRes.items.findIndex( 148 | (x) => x.id.toString().trim() === row.id.toString().trim() 149 | ) 150 | ]; 151 | 152 | if (!pkg) { 153 | return; 154 | } 155 | 156 | return ( 157 | 158 | {row.cells.map((cell) => ( 159 | <> 160 | {pkg && cell.info.header === 'id' && ( 161 | {pkg.id} 162 | )} 163 | {pkg && cell.info.header === 'created_at' && ( 164 | 165 | {new Date(pkg['created_at']).toLocaleString()} 166 | 167 | )} 168 | {pkg && cell.info.header === 'package' && ( 169 | 170 | 171 | {pkg['package']['name']} 172 | 173 | 174 | )} 175 | {pkg && cell.info.header === 'status' && ( 176 | 177 | {statusToTag(pkg['status'])} 178 | 179 | )} 180 | {pkg && cell.info.header === 'executor_username' && ( 181 | 182 | 186 | {pkg['executor_username']} 187 | 188 | 189 | )} 190 | {pkg && cell.info.header === 'extra' && ( 191 | 195 | {props.name === 'imports' && ( 196 | <> 197 | 201 | Logs 202 | 203 | {commitsToLinks( 204 | pkg['module'], 205 | pkg['package'], 206 | pkg['commits'] 207 | )} 208 | 209 | )} 210 | {props.name === 'builds' && ( 211 | <> 212 | {pkg['koji_id'] && ( 213 | 217 | Koji 218 | 219 | )} 220 | {pkg['mbs_id'] && ( 221 | 225 | MBS 226 | 227 | )} 228 | {commitsToLinks(pkg['mbs'], pkg['package'], [ 229 | pkg['import_commit'], 230 | ])} 231 | {!pkg['mbs_id'] && 232 | !pkg['koji_id'] && 233 | pkg['status'] === 'FAILED' && ( 234 | Failed during invocation 235 | )} 236 | 237 | )} 238 | 239 | )} 240 | 241 | ))} 242 | 243 | ); 244 | })} 245 | 246 |
247 | 254 |
255 | )} 256 |
257 | ) : ( 258 | 259 | ); 260 | }; 261 | -------------------------------------------------------------------------------- /ui/src/components/Root.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { 25 | Header, 26 | HeaderName, 27 | HeaderNavigation, 28 | HeaderMenuItem, 29 | } from 'carbon-components-react/lib/components/UIShell'; 30 | 31 | import 'carbon-components/css/carbon-components.min.css'; 32 | import { BrowserRouter, Link, Route, Switch } from 'react-router-dom'; 33 | import { Packages } from './Packages'; 34 | import { Dashboard } from './Dashboard'; 35 | import { BuildBatches } from './BuildBatches'; 36 | import { ImportBatches } from './ImportBatches'; 37 | import { ShowPackage } from './ShowPackage'; 38 | 39 | import '../styles/header.css'; 40 | import '../styles/tailwind.css'; 41 | import { Lookaside } from './Lookaside'; 42 | import { BuildBatchShow } from './BuildBatchShow'; 43 | import { ImportBatchShow } from './ImportBatchShow'; 44 | import { BuildsList } from './BuildsList'; 45 | import { ImportsList } from './ImportsList'; 46 | 47 | export const Root = () => { 48 | return ( 49 | 50 |
51 | 52 | [{window.SETTINGS.distribution}] 53 | 54 | 55 | 56 | Packages 57 | 58 | 59 | Builds 60 | 61 | 62 | Imports 63 | 64 | 65 | Build batches 66 | 67 | 68 | Import batches 69 | 70 | {window.STATE.authenticated && ( 71 | 72 | Lookaside 73 | 74 | )} 75 | 76 | 77 | 82 | {window.STATE.fullName || 'Login'} 83 | 84 | {window.STATE.authenticated && ( 85 | Logout 86 | )} 87 | 88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
103 |
104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /ui/src/components/ShowPackage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Button, 4 | Modal, 5 | SkeletonPlaceholder, 6 | Tag, 7 | TextInput, 8 | Tile, 9 | } from 'carbon-components-react'; 10 | import { useParams } from 'react-router-dom'; 11 | import { RouterParams } from '../misc'; 12 | import { Axios, IPackage } from '../api'; 13 | import to from 'await-to-js'; 14 | import { BuildsTable } from './BuildsTable'; 15 | import { ImportsTable } from './ImportsTable'; 16 | 17 | export const ShowPackage = () => { 18 | const [showImportModal, setShowImportModal] = React.useState(false); 19 | const [showBuildModal, setShowBuildModal] = React.useState(false); 20 | const [showSuccessModal, setShowSuccessModal] = React.useState(false); 21 | const [showResetBuildModal, setShowResetBuildModal] = React.useState(false); 22 | const [onlyBranch, setOnlyBranch] = React.useState(null); 23 | const [disable, setDisable] = React.useState(false); 24 | 25 | const params = useParams(); 26 | const [pkg, setPkg] = useState(undefined); 27 | 28 | React.useEffect(() => { 29 | (async () => { 30 | const [, res] = await to(Axios.get(`/packages/${params.id}`)); 31 | if (!res) { 32 | setPkg(null); 33 | return; 34 | } 35 | 36 | setPkg(res.data); 37 | })().then(); 38 | }, []); 39 | 40 | const queueBuild = () => { 41 | setDisable(true); 42 | 43 | (async () => { 44 | const data = { 45 | package_id: pkg.id, 46 | }; 47 | 48 | if (onlyBranch) { 49 | data['only_branch'] = onlyBranch; 50 | } 51 | 52 | const [err] = await to(Axios.post(`/builds/`, data)); 53 | if (err) { 54 | alert('API Error'); 55 | setDisable(false); 56 | return; 57 | } 58 | 59 | setShowBuildModal(false); 60 | setDisable(false); 61 | setShowSuccessModal(true); 62 | window.location.reload(); 63 | })().then(); 64 | }; 65 | 66 | const queueImport = () => { 67 | setDisable(true); 68 | 69 | (async () => { 70 | const [err] = await to( 71 | Axios.post(`/imports/`, { 72 | package_id: pkg.id, 73 | }) 74 | ); 75 | if (err) { 76 | alert('API Error'); 77 | setDisable(false); 78 | return; 79 | } 80 | 81 | setShowImportModal(false); 82 | setDisable(false); 83 | setShowSuccessModal(true); 84 | window.location.reload(); 85 | })().then(); 86 | }; 87 | 88 | const resetBuild = () => { 89 | setDisable(true); 90 | 91 | (async () => { 92 | const [err] = await to( 93 | Axios.post(`/packages/${pkg.id}/reset_latest_build`) 94 | ); 95 | if (err) { 96 | alert('API Error'); 97 | setDisable(false); 98 | return; 99 | } 100 | 101 | setShowResetBuildModal(false); 102 | setDisable(false); 103 | setShowSuccessModal(true); 104 | window.location.reload(); 105 | })().then(); 106 | }; 107 | 108 | return ( 109 | <> 110 | {pkg === undefined && } 111 | {pkg === null && ( 112 |
113 | Could not get package 114 |
115 | )} 116 | {pkg && ( 117 | <> 118 | setShowBuildModal(false)} 124 | onRequestSubmit={() => queueBuild()} 125 | > 126 |
127 | Do you want to queue this package for build? 128 |
129 | 133 | setOnlyBranch( 134 | e.currentTarget.value.trim().length === 0 135 | ? null 136 | : e.currentTarget.value 137 | ) 138 | } 139 | /> 140 |
141 | setShowImportModal(false)} 147 | onRequestSubmit={() => queueImport()} 148 | > 149 | Do you want to queue this package for import? 150 | 151 | setShowResetBuildModal(false)} 157 | onRequestSubmit={() => resetBuild()} 158 | > 159 | Are you sure you want to reset the latest build? 160 | 161 | setShowSuccessModal(false)} 167 | onRequestSubmit={() => { 168 | window.location.href = '/'; 169 | }} 170 | > 171 | Successfully queued package 172 | 173 |
174 | 175 |

{pkg.name}

176 |
177 | 178 | {pkg.signed ? 'Signed' : 'Not signed'} 179 | 180 | 181 | {pkg.is_published ? 'Published' : 'Not published'} 182 | 183 | {pkg.el8 && EL8} 184 | {pkg.is_module && Module} 185 | {pkg.is_package && Package} 186 | {pkg.part_of_module && Part of module} 187 | {pkg.repo === 'MODULAR_CANDIDATE' && ( 188 | Modular candidate 189 | )} 190 |
191 | {window.STATE.authenticated && ( 192 |
193 | 194 | 197 | {!pkg.is_published && ( 198 | 204 | )} 205 |
206 | )} 207 |
208 | 209 |
210 |
211 |
Builds
212 | { 215 | x.package = pkg; 216 | return x; 217 | }), 218 | size: pkg.builds.length, 219 | page: 0, 220 | total: pkg.builds.length, 221 | }} 222 | /> 223 |
224 |
225 |
Imports
226 | { 229 | x.package = pkg; 230 | return x; 231 | }), 232 | size: pkg.imports.length, 233 | page: 0, 234 | total: pkg.imports.length, 235 | }} 236 | /> 237 |
238 |
239 |
240 | 241 | )} 242 | 243 | ); 244 | }; 245 | -------------------------------------------------------------------------------- /ui/src/entrypoint.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import ReactDOM from 'react-dom'; 25 | import { Root } from './components/Root'; 26 | 27 | ReactDOM.render(, document.getElementById('root')); 28 | -------------------------------------------------------------------------------- /ui/src/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | export {}; 24 | 25 | interface IState { 26 | authenticated: boolean; 27 | fullName: string; 28 | } 29 | 30 | declare global { 31 | interface Window { 32 | SETTINGS: any; 33 | STATE: IState; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/misc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | export interface PageChangeEvent { 24 | page: number; 25 | pageSize: number; 26 | } 27 | 28 | export interface RouterParams { 29 | id: string; 30 | } 31 | 32 | export const changeQueryParam = (key: string, value: string) => { 33 | const url = new URL(window.location.toString()); 34 | url.searchParams.set(key, value); 35 | window.history.pushState({}, '', url.toString()); 36 | }; 37 | 38 | export const getQueryParam = (key: string): string | null => { 39 | const url = new URL(window.location.toString()); 40 | 41 | return url.searchParams.get(key); 42 | }; 43 | -------------------------------------------------------------------------------- /ui/src/styles/header.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | .bx--header__nav.right { 24 | margin-left: auto; 25 | } 26 | 27 | .bx--header__nav.right::before { 28 | display: none; 29 | } 30 | 31 | .bx--pagination { 32 | overflow: hidden !important; 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind components; 2 | @tailwind utilities; 3 | -------------------------------------------------------------------------------- /ui/src/utils/commits.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import React from 'react'; 24 | import { Commit, IPackage } from '../api'; 25 | 26 | export const commitsToLinks = ( 27 | module: boolean, 28 | pkg: IPackage, 29 | commits: Commit[] | undefined 30 | ) => { 31 | return ( 32 | commits && 33 | commits.map((commit) => ( 34 | 40 | {commit.branch} 41 | 42 | )) 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /ui/src/utils/tags.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | import { Tag } from 'carbon-components-react'; 24 | import React from 'react'; 25 | 26 | export const statusToTag = (status, merged?: boolean) => { 27 | switch (status) { 28 | case 'QUEUED': 29 | return Queued; 30 | case 'BUILDING': 31 | return Building; 32 | case 'IN_PROGRESS': 33 | return In progress; 34 | case 'SUCCEEDED': 35 | return ( 36 | 37 | Succeeded 38 | {merged !== undefined && 39 | (merged === false ? ' (Not merged)' : ' (Merged)')} 40 | 41 | ); 42 | case 'FAILED': 43 | return Failed; 44 | case 'CANCELLED': 45 | return Cancelled; 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ['src/**/*.tsx', 'public/index.html'], 4 | darkMode: false, // or 'media' or 'class' 5 | theme: { 6 | extend: {}, 7 | }, 8 | variants: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | }; 13 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "target": "ES2015", 5 | "allowSyntheticDefaultImports": true, 6 | "moduleResolution": "node" 7 | } 8 | } -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 The Distrobuild Authors 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | 23 | const path = require('path'); 24 | 25 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 26 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 27 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); 28 | const CompressionPlugin = require('compression-webpack-plugin'); 29 | const webpack = require('webpack'); 30 | 31 | const prod = process.env.NODE_ENV === 'production'; 32 | const dev = !prod; 33 | 34 | module.exports = { 35 | mode: dev ? 'development' : 'production', 36 | devtool: dev ? 'eval-cheap-module-source-map' : undefined, 37 | entry: path.resolve(__dirname, 'src/entrypoint.tsx'), 38 | output: { 39 | path: path.resolve(__dirname, 'dist'), 40 | filename: `files/distrobuild.[contenthash].js`, 41 | chunkFilename: `files/distrobuild.[id].chunk.[chunkhash].js`, 42 | publicPath: dev ? 'http://localhost:8080/static/' : '/static/', 43 | }, 44 | resolve: { 45 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.css'], 46 | }, 47 | plugins: [ 48 | new CleanWebpackPlugin({ 49 | dry: dev, 50 | }), 51 | new HtmlWebpackPlugin({ 52 | filename: 'templates/index.html', 53 | template: path.resolve(__dirname, 'public/index.html'), 54 | }), 55 | dev && new webpack.HotModuleReplacementPlugin(), 56 | dev && new ReactRefreshWebpackPlugin(), 57 | dev && new CompressionPlugin(), 58 | ].filter(Boolean), 59 | devServer: { 60 | hot: true, 61 | liveReload: false, 62 | historyApiFallback: { 63 | index: '/', 64 | }, 65 | writeToDisk: true, 66 | allowedHosts: ['http://localhost:8090'], 67 | headers: { 68 | 'Access-Control-Allow-Origin': '*', 69 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 70 | 'Access-Control-Allow-Headers': 71 | 'X-Requested-With, content-type, Authorization', 72 | }, 73 | }, 74 | module: { 75 | rules: [ 76 | { 77 | test: /\.(js|ts)x?$/, 78 | loader: 'babel-loader', 79 | exclude: [/node_modules/], 80 | }, 81 | { 82 | test: /\.css$/, 83 | use: ['style-loader', 'css-loader', 'postcss-loader'], 84 | }, 85 | ], 86 | }, 87 | }; 88 | --------------------------------------------------------------------------------