From 2b0511104d3cc00bb7e983368088556811a3b3f5 Mon Sep 17 00:00:00 2001 From: dakky Date: Wed, 13 Mar 2024 14:52:18 +0100 Subject: [PATCH 1/2] Feature: Added federated repo type Signed-off-by: dakky --- README.md | 11 ++- pyartifactory/models/__init__.py | 13 ++- pyartifactory/models/repository.py | 73 +++++++++++++++++ pyartifactory/objects/repository.py | 59 ++++++++++---- tests/test_repositories.py | 119 ++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4bc69318..d97eddf9 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,12 @@ repo = art.repositories.get_repo("repo_name") Create/Update a repository: ```python -from pyartifactory.models import LocalRepository, VirtualRepository, RemoteRepository +from pyartifactory.models import ( + LocalRepository, + VirtualRepository, + RemoteRepository, + FederatedRepository +) # Create local repo local_repo = LocalRepository(key="test_local_repo") @@ -231,6 +236,10 @@ new_virtual_repo = art.repositories.create_repo(virtual_repo) remote_repo = RemoteRepository(key="test_remote_repo") new_remote_repo = art.repositories.create_repo(remote_repo) +# Create federated repo +remote_repo = FederatedRepository(key="test_remote_repo") +new_federated_repo = art.repositories.create_repo(remote_repo) + # Update a repository local_repo = art.repositories.get_repo("test_local_repo") local_repo.description = "test_local_repo" diff --git a/pyartifactory/models/__init__.py b/pyartifactory/models/__init__.py index 79719b56..f83cce4d 100644 --- a/pyartifactory/models/__init__.py +++ b/pyartifactory/models/__init__.py @@ -16,6 +16,8 @@ from .group import Group, SimpleGroup from .permission import Permission, PermissionV2, SimplePermission from .repository import ( + FederatedRepository, + FederatedRepositoryResponse, LocalRepository, LocalRepositoryResponse, RemoteRepository, @@ -26,9 +28,14 @@ ) from .user import BaseUserModel, NewUser, SimpleUser, User, UserResponse -AnyRepositoryResponse = Union[LocalRepositoryResponse, VirtualRepositoryResponse, RemoteRepositoryResponse] +AnyRepositoryResponse = Union[ + LocalRepositoryResponse, + VirtualRepositoryResponse, + RemoteRepositoryResponse, + FederatedRepositoryResponse, +] -AnyRepository = Union[LocalRepository, VirtualRepository, RemoteRepository] +AnyRepository = Union[LocalRepository, VirtualRepository, RemoteRepository, FederatedRepository] AnyPermission = Union[Permission, PermissionV2] __all__ = [ @@ -50,6 +57,8 @@ "LocalRepositoryResponse", "RemoteRepository", "RemoteRepositoryResponse", + "FederatedRepository", + "FederatedRepositoryResponse", "SimpleRepository", "VirtualRepository", "VirtualRepositoryResponse", diff --git a/pyartifactory/models/repository.py b/pyartifactory/models/repository.py index 0615959b..039848bb 100644 --- a/pyartifactory/models/repository.py +++ b/pyartifactory/models/repository.py @@ -51,6 +51,7 @@ class RClassEnum(str, Enum): local = "local" virtual = "virtual" remote = "remote" + federated = "federated" class ChecksumPolicyType(str, Enum): @@ -114,6 +115,18 @@ class ContentSynchronisation(BaseModel): source: Source = Source() +class FederatedMembers(BaseModel): + url: str = "" + enabled: Literal["true", "false"] = "true" + + +class FederatedMembersResponse(BaseModel): + """Models a federated member response.""" + + url: str = "" + enabled: bool = True + + class SimpleRepository(BaseModel): """Models a simple repository.""" @@ -306,3 +319,63 @@ class RemoteRepositoryResponse(RemoteRepository): enableVagrantSupport: bool = False enableGitLfsSupport: bool = False enableDistRepoSupport: bool = False + + +class FederatedBaseRepostoryModel(BaseRepositoryModel): + """ + Models a basic federated repo without members as they can't be overwritten + and differ in response and request + """ + + rclass: Literal[RClassEnum.federated] = RClassEnum.federated + checksumPolicyType: ChecksumPolicyType = ChecksumPolicyType.client_checksums + handleReleases: bool = True + handleSnapshots: bool = True + maxUniqueSnapshots: int = 0 + maxUniqueTags: int = 0 + debianTrivialLayout: bool = False + snapshotVersionBehavior: SnapshotVersionBehavior = SnapshotVersionBehavior.non_unique + suppressPomConsistencyChecks: bool = False + blackedOut: bool = False + xrayIndex: bool = False + propertySets: Optional[List[str]] = None + dockerApiVersion: str = "V2" + archiveBrowsingEnabled: bool = False + calculateYumMetadata: bool = False + yumRootDepth: int = 0 + enableFileListsIndexing: bool = False + optionalIndexCompressionFormats: Optional[List[str]] = None + downloadRedirect: bool = False + cdnRedirect: bool = False + blockPushingSchema1: bool = False + primaryKeyPairRef: Optional[str] = None + secondaryKeyPairRef: Optional[str] = None + priorityResolution: bool = False + cargoInternalIndex: bool = False + + +class FederatedRepository(FederatedBaseRepostoryModel): + """Models a federated Repository (member model differs from reponse).""" + + members: List[FederatedMembers] = [] + + +class FederatedRepositoryResponse(FederatedBaseRepostoryModel): + """Models a federated repository response (member model differs from request).""" + + members: List[FederatedMembersResponse] = [] + enableComposerSupport: bool = False + enableNuGetSupport: bool = False + enableGemsSupport: bool = False + enableNpmSupport: bool = False + enableBowerSupport: bool = False + enableCocoaPodsSupport: bool = False + enableConanSupport: bool = False + enableDebianSupport: bool = False + enablePypiSupport: bool = False + enablePuppetSupport: bool = False + enableDockerSupport: bool = False + forceNugetAuthentication: bool = False + enableVagrantSupport: bool = False + enableGitLfsSupport: bool = False + enableDistRepoSupport: bool = False diff --git a/pyartifactory/objects/repository.py b/pyartifactory/objects/repository.py index 269c50f8..206aed03 100644 --- a/pyartifactory/objects/repository.py +++ b/pyartifactory/objects/repository.py @@ -5,14 +5,16 @@ from typing import List, Union, overload import requests -from pydantic import ValidationError from requests import Response from pyartifactory.exception import ArtifactoryError, RepositoryAlreadyExistsError, RepositoryNotFoundError from pyartifactory.models import AnyRepository, AnyRepositoryResponse from pyartifactory.models.repository import ( + FederatedRepository, + FederatedRepositoryResponse, LocalRepository, LocalRepositoryResponse, + RClassEnum, RemoteRepository, RemoteRepositoryResponse, SimpleRepository, @@ -35,18 +37,32 @@ def get_repo(self, repo_name: str) -> AnyRepositoryResponse: """ Finds repository in artifactory. Raises an exception if the repo doesn't exist. :param repo_name: Name of the repository to retrieve - :return: Either a local, virtual or remote repository + :return: Either a local, virtual, remote or federated repository """ try: response = self._get(f"api/{self._uri}/{repo_name}") + response_data = response.json() + rclass = None + try: - artifact_info: AnyRepositoryResponse = LocalRepositoryResponse.model_validate(response.json()) - except ValidationError: - try: - artifact_info = VirtualRepositoryResponse.model_validate(response.json()) - except ValidationError: - artifact_info = RemoteRepositoryResponse.model_validate(response.json()) - return artifact_info + rclass = response_data["rclass"] + except KeyError: + raise KeyError('"rclass" key not found in the response data received by artifactory.') + + # Match to the correct repository type depending on the rclass + if rclass == RClassEnum.local: + return LocalRepositoryResponse.model_validate(response_data) + elif rclass == RClassEnum.virtual: + return VirtualRepositoryResponse.model_validate(response_data) + elif rclass == RClassEnum.remote: + return RemoteRepositoryResponse.model_validate(response_data) + elif rclass == RClassEnum.federated: + return FederatedRepositoryResponse.model_validate(response_data) + else: + # this should never happen and is a missing repotype in the library + raise ArtifactoryError( + f"Unknown repository type found in response: {rclass}. Please report this issue.", + ) except requests.exceptions.HTTPError as error: http_response: Union[Response, None] = error.response if isinstance(http_response, Response) and http_response.status_code in (404, 400): @@ -66,11 +82,19 @@ def create_repo(self, repo: VirtualRepository) -> VirtualRepositoryResponse: def create_repo(self, repo: RemoteRepository) -> RemoteRepositoryResponse: ... - def create_repo(self, repo: AnyRepository) -> AnyRepositoryResponse: + @overload + def create_repo(self, repo: FederatedRepository) -> FederatedRepositoryResponse: + ... + + def create_repo( + self, + repo: AnyRepository, + ) -> AnyRepositoryResponse: """ - Creates a local, virtual or remote repository - :param repo: Either a local, virtual or remote repository - :return: LocalRepositoryResponse, VirtualRepositoryResponse or RemoteRepositoryResponse object + Creates a local, virtual, remote or federated repository + :param repo: Either a local, virtual, remote or federated repository + :return: LocalRepositoryResponse, VirtualRepositoryResponse, RemoteRepositoryResponse + or FederatedRepositoryResponse object """ repo_name = repo.key try: @@ -99,7 +123,14 @@ def update_repo(self, repo: VirtualRepository) -> VirtualRepositoryResponse: def update_repo(self, repo: RemoteRepository) -> RemoteRepositoryResponse: ... - def update_repo(self, repo: AnyRepository) -> AnyRepositoryResponse: + @overload + def update_repo(self, repo: FederatedRepository) -> FederatedRepositoryResponse: + ... + + def update_repo( + self, + repo: AnyRepository, + ) -> AnyRepositoryResponse: """ Updates a local, virtual or remote repository :param repo: Either a local, virtual or remote repository diff --git a/tests/test_repositories.py b/tests/test_repositories.py index cd472106..a83d8ab5 100644 --- a/tests/test_repositories.py +++ b/tests/test_repositories.py @@ -8,6 +8,8 @@ from pyartifactory.exception import RepositoryAlreadyExistsError, RepositoryNotFoundError from pyartifactory.models import ( AuthModel, + FederatedRepository, + FederatedRepositoryResponse, LocalRepository, LocalRepositoryResponse, RemoteRepository, @@ -42,6 +44,29 @@ description="updated", ) +FEDERATED_REPOSITORY = FederatedRepository( + key="test_federated_repository", + url="http://test-url.com", + members=[{"url": "member1.domain.com", "enabled": True}], +) +FEDERATED_REPOSITORY_RESPONSE = FederatedRepositoryResponse( + key="test_federated_repository", + url="http://test-url.com", + members=[{"url": "member1.domain.com", "enabled": True}], +) +UPDATED_FEDERATED_REPOSITORY = FederatedRepository( + key="test_federated_repository", + url="http://test-url.com", + description="updated", + members=[{"url": "member1.domain.com", "enabled": True}], +) +UPDATED_FEDERATED_REPOSITORY_RESPONSE = FederatedRepositoryResponse( + key="test_federated_repository", + url="http://test-url.com", + description="updated", + members=[{"url": "member1.domain.com", "enabled": True}], +) + @responses.activate def test_create_local_repository_fail_if_repository_already_exists( @@ -100,6 +125,25 @@ def test_create_remote_repository_fail_if_repository_already_exists( artifactory_repo.get_repo.assert_called_once_with(REMOTE_REPOSITORY.key) +@responses.activate +def test_create_federated_repository_fail_if_repository_already_exists( + mocker, +): + responses.add( + responses.GET, + f"{URL}/api/repositories/{FEDERATED_REPOSITORY.key}", + json=FEDERATED_REPOSITORY_RESPONSE.model_dump(), + status=200, + ) + + artifactory_repo = ArtifactoryRepository(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_repo, "get_repo") + with pytest.raises(RepositoryAlreadyExistsError): + artifactory_repo.create_repo(FEDERATED_REPOSITORY) + + artifactory_repo.get_repo.assert_called_once_with(FEDERATED_REPOSITORY.key) + + @responses.activate def test_create_local_repository_success(mocker): responses.add(responses.GET, f"{URL}/api/repositories/{LOCAL_REPOSITORY.key}", status=404) @@ -170,6 +214,29 @@ def test_create_remote_repository_success(mocker): assert remote_repo == REMOTE_REPOSITORY_RESPONSE +@responses.activate +def test_create_federated_repository_success(mocker): + responses.add(responses.GET, f"{URL}/api/repositories/{FEDERATED_REPOSITORY.key}", status=404) + responses.add( + responses.PUT, + f"{URL}/api/repositories/{FEDERATED_REPOSITORY.key}", + json=FEDERATED_REPOSITORY_RESPONSE.model_dump(), + status=201, + ) + responses.add( + responses.GET, + f"{URL}/api/repositories/{FEDERATED_REPOSITORY.key}", + json=FEDERATED_REPOSITORY_RESPONSE.model_dump(), + status=200, + ) + + artifactory_repo = ArtifactoryRepository(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_repo, "get_repo") + federated_repo = artifactory_repo.create_repo(FEDERATED_REPOSITORY) + + assert federated_repo == FEDERATED_REPOSITORY_RESPONSE + + @responses.activate def test_get_local_repository_error_not_found(mocker): responses.add(responses.GET, f"{URL}/api/repositories/{LOCAL_REPOSITORY.key}", status=404) @@ -225,6 +292,21 @@ def test_get_remote_repository_success(): assert remote_repo == REMOTE_REPOSITORY_RESPONSE +@responses.activate +def test_get_federated_repository_success(): + responses.add( + responses.GET, + f"{URL}/api/repositories/{FEDERATED_REPOSITORY.key}", + json=FEDERATED_REPOSITORY_RESPONSE.model_dump(), + status=200, + ) + + artifactory_repo = ArtifactoryRepository(AuthModel(url=URL, auth=AUTH)) + remote_repo = artifactory_repo.get_repo(FEDERATED_REPOSITORY.key) + + assert remote_repo == FEDERATED_REPOSITORY_RESPONSE + + @responses.activate def test_list_repositories_success(mocker): responses.add( @@ -276,6 +358,18 @@ def test_update_remote_repository_fail_if_repo_not_found(mocker): artifactory_repo.get_repo.assert_called_once_with(REMOTE_REPOSITORY.key) +@responses.activate +def test_update_federated_repository_fail_if_repo_not_found(mocker): + responses.add(responses.GET, f"{URL}/api/repositories/{FEDERATED_REPOSITORY.key}", status=404) + + artifactory_repo = ArtifactoryRepository(AuthModel(url=URL, auth=AUTH)) + mocker.spy(artifactory_repo, "get_repo") + with pytest.raises(RepositoryNotFoundError): + artifactory_repo.update_repo(FEDERATED_REPOSITORY) + + artifactory_repo.get_repo.assert_called_once_with(FEDERATED_REPOSITORY.key) + + @responses.activate def test_update_local_repository_success(mocker): responses.add( @@ -354,6 +448,31 @@ def test_update_remote_repository_success(): assert updated_repo == UPDATED_REMOTE_REPOSITORY_RESPONSE +@responses.activate +def test_update_federated_repository_success(): + responses.add( + responses.GET, + f"{URL}/api/repositories/{UPDATED_FEDERATED_REPOSITORY.key}", + json=UPDATED_FEDERATED_REPOSITORY_RESPONSE.model_dump(), + status=200, + ) + + responses.add( + responses.POST, + f"{URL}/api/repositories/{UPDATED_FEDERATED_REPOSITORY.key}", + status=200, + ) + responses.add( + responses.GET, + f"{URL}/api/repositories/{UPDATED_FEDERATED_REPOSITORY.key}", + json=UPDATED_FEDERATED_REPOSITORY_RESPONSE.model_dump(), + status=200, + ) + artifactory_repo = ArtifactoryRepository(AuthModel(url=URL, auth=AUTH)) + updated_repo = artifactory_repo.update_repo(UPDATED_FEDERATED_REPOSITORY) + assert updated_repo == UPDATED_FEDERATED_REPOSITORY_RESPONSE + + @responses.activate def test_delete_repo_fail_if_repo_not_found(): responses.add(responses.DELETE, f"{URL}/api/repositories/{REMOTE_REPOSITORY.key}", status=404) From 09bf22072a9cfdaafeb3efd6ec741fe25822c399 Mon Sep 17 00:00:00 2001 From: dakky Date: Thu, 25 Apr 2024 12:09:32 +0200 Subject: [PATCH 2/2] Fixed some test issues concerning federated repos Signed-off-by: dakky --- tests/test_repositories.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_repositories.py b/tests/test_repositories.py index a83d8ab5..6a889f30 100644 --- a/tests/test_repositories.py +++ b/tests/test_repositories.py @@ -47,24 +47,24 @@ FEDERATED_REPOSITORY = FederatedRepository( key="test_federated_repository", url="http://test-url.com", - members=[{"url": "member1.domain.com", "enabled": True}], + members=[{"url": "member1.domain.com", "enabled": "true"}], ) FEDERATED_REPOSITORY_RESPONSE = FederatedRepositoryResponse( key="test_federated_repository", url="http://test-url.com", - members=[{"url": "member1.domain.com", "enabled": True}], + members=[{"url": "member1.domain.com", "enabled": "true"}], ) UPDATED_FEDERATED_REPOSITORY = FederatedRepository( key="test_federated_repository", url="http://test-url.com", description="updated", - members=[{"url": "member1.domain.com", "enabled": True}], + members=[{"url": "member1.domain.com", "enabled": "true"}], ) UPDATED_FEDERATED_REPOSITORY_RESPONSE = FederatedRepositoryResponse( key="test_federated_repository", url="http://test-url.com", description="updated", - members=[{"url": "member1.domain.com", "enabled": True}], + members=[{"url": "member1.domain.com", "enabled": "true"}], )