├── .github └── workflows │ ├── build.yml │ ├── docs.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codegen ├── README.md ├── ast │ ├── fragments │ │ ├── main │ │ │ ├── artifact.py │ │ │ ├── extraattrs.py │ │ │ ├── generalinfo.py │ │ │ ├── immutablerule.py │ │ │ ├── nativereportsummary.py │ │ │ ├── projectmetadata.py │ │ │ ├── registryproviders.py │ │ │ ├── replicationfilter.py │ │ │ ├── repository.py │ │ │ ├── retentionrule.py │ │ │ └── vulnerabilitysummary.py │ │ └── scanner │ │ │ ├── harborvulnerabilityreport.py │ │ │ ├── scanner.py │ │ │ ├── severity.py │ │ │ └── vulnerabilityitem.py │ └── parser.py └── generate.sh ├── docs ├── endpoints │ ├── artifacts.md │ ├── auditlogs.md │ ├── configure.md │ ├── cveallowlist.md │ ├── gc.md │ ├── health.md │ ├── icon.md │ ├── immutable.md │ ├── index.md │ ├── labels.md │ ├── ldap.md │ ├── oidc.md │ ├── permissions.md │ ├── ping.md │ ├── projectmetadata.md │ ├── projects.md │ ├── purge.md │ ├── quota.md │ ├── registries.md │ ├── replication.md │ ├── repositories.md │ ├── retention.md │ ├── robots.md │ ├── robotv1.md │ ├── scan.md │ ├── scanall.md │ ├── scanexport.md │ ├── scanners.md │ ├── search.md │ ├── statistics.md │ ├── systeminfo.md │ ├── usergroups.md │ ├── users.md │ └── webhooks.md ├── img │ └── usage │ │ └── models │ │ ├── autocomplete0.png │ │ ├── autocomplete1.png │ │ ├── autocomplete2.png │ │ ├── autocomplete3.png │ │ ├── autocomplete4.png │ │ ├── autocomplete5.png │ │ └── autocomplete6.png ├── index.md ├── recipes │ ├── artifacts │ │ ├── delete-artifact.md │ │ ├── get-artifact-scan-overview.md │ │ ├── get-artifact-vulnerabilities.md │ │ ├── get-artifact.md │ │ └── get-artifacts.md │ ├── client │ │ └── reauth.md │ ├── ext │ │ ├── artifact-vulns.md │ │ ├── artifactowner.md │ │ ├── conc-artifact.md │ │ ├── conc-repo.md │ │ └── index.md │ ├── index.md │ ├── projects │ │ ├── create-project.md │ │ ├── delete-project.md │ │ ├── get-project.md │ │ ├── get-projects.md │ │ └── update-project.md │ ├── repos │ │ ├── get-repo.md │ │ ├── get-repos-project.md │ │ └── get-repos.md │ ├── retention │ │ └── get-retention.md │ ├── scan │ │ └── scan-artifact.md │ └── user │ │ └── get-current-user.md ├── reference │ ├── auth.md │ ├── client.md │ ├── client_sync.md │ ├── exceptions.md │ ├── ext │ │ ├── api.md │ │ ├── artifact.md │ │ ├── cve.md │ │ └── report.md │ ├── index.md │ ├── models │ │ ├── _models.md │ │ ├── _scanner.md │ │ ├── base.md │ │ ├── buildhistory.md │ │ ├── mappings.md │ │ ├── models.md │ │ ├── oidc.md │ │ └── scanner.md │ ├── responselog.md │ ├── retry.md │ ├── types.md │ └── utils.md └── usage │ ├── async-sync.md │ ├── authentication.md │ ├── creating-system-robot.md │ ├── exceptions.md │ ├── ext │ ├── _example_callback.py │ ├── _example_get_artifact.py │ ├── api.md │ ├── artifact.md │ ├── index.md │ └── report.md │ ├── index.md │ ├── logging.md │ ├── methods │ ├── create-update.md │ ├── delete.md │ ├── index.md │ └── read.md │ ├── models.md │ ├── responselog.md │ ├── retry.md │ ├── rich.md │ └── validation.md ├── harborapi ├── __about__.py ├── __init__.py ├── _types.py ├── auth.py ├── client.py ├── client_sync.py ├── exceptions.py ├── ext │ ├── __init__.py │ ├── api.py │ ├── artifact.py │ ├── cve.py │ ├── regex.py │ ├── report.py │ └── stats.py ├── log.py ├── models │ ├── __init__.py │ ├── _models.py │ ├── _scanner.py │ ├── base.py │ ├── buildhistory.py │ ├── file.py │ ├── mappings.py │ ├── models.py │ ├── oidc.py │ └── scanner.py ├── py.typed ├── responselog.py ├── retry.py ├── utils.py └── version.py ├── mkdocs.yml ├── pyproject.toml ├── scripts └── bump_version.py └── tests ├── __init__.py ├── conftest.py ├── endpoints ├── __init__.py ├── test_artifacts.py ├── test_audit.py ├── test_configure.py ├── test_cveallowlist.py ├── test_gc.py ├── test_health.py ├── test_icon.py ├── test_immutable.py ├── test_labels.py ├── test_ldap.py ├── test_oidc.py ├── test_permissions.py ├── test_ping.py ├── test_projectmetadata.py ├── test_projects.py ├── test_purge.py ├── test_quota.py ├── test_registries.py ├── test_replication.py ├── test_repositories.py ├── test_retention.py ├── test_robot_v1.py ├── test_robots.py ├── test_scan.py ├── test_scanall.py ├── test_scanexport.py ├── test_scanners.py ├── test_search.py ├── test_statistic.py ├── test_systeminfo.py ├── test_usergroups.py ├── test_users.py └── test_webhooks.py ├── ext ├── __init__.py ├── test_artifact.py └── test_report.py ├── models ├── __init__.py ├── _test_models.py ├── test_base.py ├── test_file.py ├── test_mappings.py ├── test_models_compat.py ├── test_projectmetadata.py ├── test_rich.py ├── test_scanner.py └── utils.py ├── strategies ├── __init__.py ├── artifact.py ├── cveallowlist.py ├── errors.py └── ext.py ├── test_auth.py ├── test_client.py ├── test_client_sync.py ├── test_log.py ├── test_responselog.py ├── test_retry.py ├── test_utils.py ├── test_version.py └── utils.py /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build harborapi 2 | 3 | on: 4 | push: 5 | tags: 6 | - harborapi-v* 7 | 8 | concurrency: 9 | group: build-harborapi-${{ github.head_ref }} 10 | 11 | jobs: 12 | build: 13 | name: Build wheels and source distribution 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install build dependencies 20 | run: python -m pip install --upgrade build 21 | 22 | - name: Build source distribution 23 | run: python -m build 24 | 25 | - uses: actions/upload-artifact@v4 26 | with: 27 | name: artifacts 28 | path: dist/* 29 | if-no-files-found: error 30 | 31 | publish: 32 | name: Publish release 33 | needs: 34 | - build 35 | runs-on: ubuntu-latest 36 | 37 | steps: 38 | - uses: actions/download-artifact@v4 39 | with: 40 | name: artifacts 41 | path: dist 42 | 43 | - name: Push build artifacts to PyPI 44 | uses: pypa/gh-action-pypi-publish@release/v1 45 | with: 46 | skip-existing: true 47 | user: __token__ 48 | password: ${{ secrets.PYPI_API_TOKEN_HARBORAPI }} 49 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - hatch 7 | paths: 8 | - ".github/workflows/docs.yml" 9 | - "docs/**" 10 | - "mkdocs.yml" 11 | - "harborapi/**" 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.10" 23 | 24 | - name: Ensure latest pip 25 | run: python -m pip install --upgrade pip 26 | 27 | - name: Install ourself 28 | run: | 29 | pip install -e . 30 | 31 | - name: Install hatch 32 | run: pip install hatch 33 | 34 | - name: Build documentation and publish 35 | run: hatch run docs:mkdocs gh-deploy --force 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "docs/**" 9 | - "*.md" 10 | - "mkdocs.yml" 11 | - ".github/workflows/docs.yml" 12 | - ".github/workflows/project.yml" 13 | pull_request: 14 | branches: 15 | - main 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} 19 | cancel-in-progress: true 20 | 21 | env: 22 | PYTHONUNBUFFERED: "1" 23 | FORCE_COLOR: "1" 24 | 25 | jobs: 26 | run: 27 | name: Python ${{ matrix.python-version }} on ${{ startsWith(matrix.os, 'macos-') && 'macOS' || startsWith(matrix.os, 'windows-') && 'Windows' || 'Linux' }} 28 | runs-on: ${{ matrix.os }} 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | os: [ubuntu-latest] 33 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Set up Python ${{ matrix.python-version }} 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | 43 | - name: Ensure latest pip 44 | run: python -m pip install --upgrade pip 45 | 46 | - name: Install ourself 47 | run: pip install -e . 48 | 49 | - name: Install Hatch 50 | run: pip install hatch 51 | 52 | - name: Run tests 53 | run: hatch run test -vv 54 | env: 55 | HYPOTHESIS_PROFILE: debug 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dev directories 2 | venv/ 3 | ._dev/ 4 | .vscode/ 5 | codegen/temp 6 | 7 | # Single files 8 | swagger.yaml 9 | .coverage 10 | .DS_Store 11 | 12 | # Cache 13 | .pytest_cache 14 | .mypy_cache 15 | .hypothesis 16 | *.py[ci] 17 | 18 | # Packaging 19 | dist 20 | build 21 | *.egg-info 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.5.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/astral-sh/ruff-pre-commit 12 | # Ruff version. 13 | rev: v0.4.8 14 | hooks: 15 | # Run the linter. 16 | - id: ruff 17 | args: [ --fix ] 18 | exclude: '^codegen/ast/fragments/.*' 19 | # Run the formatter. 20 | - id: ruff-format 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-present University of Oslo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/artifact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Artifact(BaseModel): 5 | @property 6 | def scan(self) -> Optional[NativeReportSummary]: 7 | """ 8 | Returns the first scan overview found for the Artifact, 9 | or None if there are none. 10 | 11 | Artifacts are typically scanned in a single format, represented 12 | by its MIME type. Thus, most Artifacts will have only one 13 | scan overview. This property provides a quick access to it. 14 | """ 15 | if self.scan_overview and self.scan_overview.root: 16 | return self.scan_overview.root[next(iter(self.scan_overview))] 17 | return None 18 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/extraattrs.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Dict 5 | from typing import Optional 6 | 7 | from pydantic import RootModel 8 | 9 | 10 | class ExtraAttrs(RootModel[Optional[Dict[str, Any]]]): 11 | pass 12 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/generalinfo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | from pydantic import Field 6 | 7 | 8 | class GeneralInfo(BaseModel): 9 | with_chartmuseum: Optional[bool] = Field( 10 | default=None, 11 | description="DEPRECATED: Harbor instance is deployed with nested chartmuseum.", 12 | ) 13 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/immutablerule.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Dict 5 | from typing import Optional 6 | 7 | # Changed: change params field type 8 | # Reason: params is a dict of Any, not a dict of dicts 9 | 10 | 11 | class ImmutableRule(BaseModel): 12 | params: Optional[Dict[str, Any]] = None 13 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/nativereportsummary.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .scanner import Severity 4 | 5 | 6 | class NativeReportSummary(BaseModel): 7 | @property 8 | def severity_enum(self) -> Optional[Severity]: 9 | """The severity of the vulnerability 10 | 11 | Returns 12 | ------- 13 | Optional[Severity] 14 | The severity of the vulnerability 15 | """ 16 | if self.severity: 17 | return Severity(self.severity) 18 | return None 19 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/projectmetadata.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Optional 5 | from typing import Type as PyType 6 | from typing import Union 7 | 8 | from pydantic import Field 9 | from pydantic import ValidationInfo 10 | from pydantic import field_validator 11 | 12 | 13 | class ProjectMetadata(BaseModel): 14 | retention_id: Optional[Union[str, int]] = Field( 15 | default=None, description="The ID of the tag retention policy for the project" 16 | ) 17 | 18 | @field_validator("*", mode="before") 19 | @classmethod 20 | def _validate_strbool( 21 | cls: PyType["BaseModel"], 22 | v: Any, 23 | info: ValidationInfo, 24 | ) -> Any: 25 | """The project metadata model spec specifies that all fields are 26 | strings, but their valid values are 'true' and 'false'. 27 | 28 | Pydantic has built-in conversion from bool to str, but it yields 29 | 'True' and 'False' instead of 'true' and 'false'. This validator 30 | converts bools to the strings 'true' and 'false' instead. 31 | 32 | This validator only converts the values if the field 33 | description contains the word '"true"' (with double quotes). 34 | """ 35 | if not isinstance(v, bool): 36 | return v 37 | if not info.field_name: 38 | raise ValueError("Validator is not attached to a field.") 39 | field = cls.model_fields[info.field_name] 40 | 41 | if not field.description or '"true"' not in field.description: 42 | return v 43 | return str(v).lower() 44 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/registryproviders.py: -------------------------------------------------------------------------------- 1 | # /replication/adapterinfos returns a dict of RegistryProviderInfo objects, 2 | # where each key is the name of registry provider. 3 | # There is no model for this in the spec. 4 | from __future__ import annotations 5 | 6 | from typing import Dict 7 | 8 | from pydantic import Field 9 | 10 | 11 | class RegistryProviders(RootModel[Dict[str, RegistryProviderInfo]]): 12 | root: Dict[str, RegistryProviderInfo] = Field( 13 | default={}, 14 | description="The registry providers. Each key is the name of the registry provider.", 15 | ) 16 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/replicationfilter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Dict 5 | from typing import Union 6 | 7 | from pydantic import Field 8 | 9 | 10 | class ReplicationFilter(BaseModel): 11 | value: Union[str, Dict[str, Any], None] = Field( 12 | default=None, description="The value of replication policy filter." 13 | ) 14 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/repository.py: -------------------------------------------------------------------------------- 1 | """Fragment that adds new properties and methods to the Repository model""" 2 | from __future__ import annotations 3 | 4 | from typing import Optional 5 | from typing import Tuple 6 | 7 | from ..log import logger 8 | 9 | 10 | class Repository(BaseModel): 11 | @property 12 | def base_name(self) -> str: 13 | """The repository name without the project name 14 | 15 | Returns 16 | ------- 17 | Optional[str] 18 | The basename of the repository name 19 | """ 20 | s = self.split_name() 21 | return s[1] if s else "" 22 | 23 | @property 24 | def project_name(self) -> str: 25 | """The name of the project that the repository belongs to 26 | 27 | Returns 28 | ------- 29 | Optional[str] 30 | The name of the project that the repository belongs to 31 | """ 32 | s = self.split_name() 33 | return s[0] if s else "" 34 | 35 | # TODO: cache? 36 | def split_name(self) -> Optional[Tuple[str, str]]: 37 | """Split name into tuple of project and repository name 38 | 39 | Returns 40 | ------- 41 | Optional[Tuple[str, str]] 42 | Tuple of project name and repo name 43 | """ 44 | if not self.name: 45 | return None 46 | components = self.name.split("/", 1) 47 | if len(components) != 2: # no slash in name 48 | # Shouldn't happen, but we account for it anyway 49 | logger.warning( 50 | "Repository name '%s' is not in the format /", self.name 51 | ) 52 | return None 53 | return components[0], components[1] 54 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/retentionrule.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Dict 5 | from typing import Optional 6 | 7 | # Changed: change params field type 8 | # Reason: params is a dict of Any, not a dict of dicts 9 | # TODO: add descriptions 10 | 11 | 12 | class RetentionRule(BaseModel): 13 | params: Optional[Dict[str, Any]] = None 14 | -------------------------------------------------------------------------------- /codegen/ast/fragments/main/vulnerabilitysummary.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Dict 5 | 6 | from pydantic import Field 7 | from pydantic import model_validator 8 | 9 | 10 | class VulnerabilitySummary(BaseModel): 11 | # Summary dict keys added as fields 12 | critical: int = Field( 13 | default=0, 14 | alias="Critical", 15 | description="The number of critical vulnerabilities detected.", 16 | ) 17 | high: int = Field( 18 | default=0, 19 | alias="High", 20 | description="The number of critical vulnerabilities detected.", 21 | ) 22 | medium: int = Field( 23 | default=0, 24 | alias="Medium", 25 | description="The number of critical vulnerabilities detected.", 26 | ) 27 | low: int = Field( 28 | default=0, 29 | alias="Low", 30 | description="The number of critical vulnerabilities detected.", 31 | ) 32 | unknown: int = Field( 33 | default=0, 34 | alias="Unknown", 35 | description="The number of critical vulnerabilities detected.", 36 | ) 37 | 38 | @model_validator(mode="before") 39 | @classmethod 40 | def _assign_severity_breakdown(cls, values: Dict[str, Any]) -> Dict[str, Any]: 41 | summary = values.get("summary") or {} # account for None 42 | if not isinstance(summary, dict): 43 | raise ValueError("'summary' must be a dict") 44 | return {**values, **summary} 45 | -------------------------------------------------------------------------------- /codegen/ast/fragments/scanner/scanner.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from ..version import SemVer 4 | from ..version import get_semver 5 | 6 | 7 | class Scanner(BaseModel): 8 | @property 9 | def semver(self) -> SemVer: 10 | return get_semver(self.version) 11 | -------------------------------------------------------------------------------- /codegen/ast/fragments/scanner/severity.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict 4 | from typing import Final 5 | from typing import List 6 | from typing import Tuple 7 | 8 | 9 | class Severity(Enum): 10 | # adds `none` to the enum. Unknown what it signifies, but it has been observed 11 | # in responses from the API. 12 | none = "None" 13 | 14 | def __gt__(self, other: Severity) -> bool: 15 | return SEVERITY_PRIORITY[self] > SEVERITY_PRIORITY[other] 16 | 17 | def __ge__(self, other: Severity) -> bool: 18 | return SEVERITY_PRIORITY[self] >= SEVERITY_PRIORITY[other] 19 | 20 | def __lt__(self, other: Severity) -> bool: 21 | return SEVERITY_PRIORITY[self] < SEVERITY_PRIORITY[other] 22 | 23 | def __le__(self, other: Severity) -> bool: 24 | return SEVERITY_PRIORITY[self] <= SEVERITY_PRIORITY[other] 25 | 26 | 27 | SEVERITY_PRIORITY: Final[Dict[Severity, int]] = { 28 | Severity.none: 0, # added by fragment 29 | Severity.unknown: 1, 30 | Severity.negligible: 2, 31 | Severity.low: 3, 32 | Severity.medium: 4, 33 | Severity.high: 5, 34 | Severity.critical: 6, 35 | } 36 | """The priority of severity levels, from lowest to highest. Used for sorting.""" 37 | 38 | 39 | def most_severe(severities: Iterable[Severity]) -> Severity: 40 | """Returns the highest severity in a list of severities.""" 41 | return max(severities, key=lambda x: SEVERITY_PRIORITY[x], default=Severity.unknown) 42 | 43 | 44 | def sort_distribution(distribution: "Counter[Severity]") -> List[Tuple[Severity, int]]: 45 | """Turn a counter of Severities into a sorted list of (severity, count) tuples.""" 46 | return [ 47 | (k, v) 48 | for k, v in sorted(distribution.items(), key=lambda x: SEVERITY_PRIORITY[x[0]]) 49 | ] 50 | -------------------------------------------------------------------------------- /codegen/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Display help message 4 | if [[ "$1" == "--help" ]]; then 5 | echo "USAGE: generate.sh [main|scanner] [--no-fetch]" 6 | exit 0 7 | fi 8 | 9 | # TODO: use https://github.com/RonnyPfannschmidt/prance 10 | # instead of relying on swagger converter API 11 | 12 | # Initialize variable to determine whether codegen should run 13 | fetch_spec=true 14 | 15 | # Default values for source_url, output_file, final_file, and source_type 16 | source_url="https://raw.githubusercontent.com/goharbor/harbor/main/api/v2.0/swagger.yaml" 17 | output_file="./codegen/temp/_models.py" 18 | final_file="./codegen/temp/models.py" 19 | source_type="main" # default source type 20 | 21 | # Check for optional positional argument for source ('main' or 'scanner') 22 | if [[ "$1" == "scanner" ]]; then 23 | source_url="https://raw.githubusercontent.com/goharbor/pluggable-scanner-spec/master/api/spec/scanner-adapter-openapi-v1.1.yaml" 24 | output_file="./codegen/temp/_scanner.py" 25 | final_file="./codegen/temp/scanner.py" 26 | source_type="scanner" 27 | elif [[ "$1" != "" && "$1" != "main" ]]; then 28 | echo "Invalid argument: $1. Please use 'main' or 'scanner'. Defaulting to 'main'." 29 | fi 30 | 31 | # Loop through command line arguments for flags 32 | for arg in "$@"; do 33 | case $arg in 34 | --no-fetch) 35 | fetch_spec=false 36 | shift # Remove --no-fetch from processing 37 | ;; 38 | *) 39 | shift # Remove generic argument from processing 40 | ;; 41 | esac 42 | done 43 | 44 | mkdir -p ./codegen/temp 45 | 46 | # Run datamodel-codegen only if fetch_spec is true 47 | if [ "$fetch_spec" = true ]; then 48 | datamodel-codegen \ 49 | --url "https://converter.swagger.io/api/convert?url=$source_url" \ 50 | --output "$output_file" 51 | fi 52 | 53 | python codegen/ast/parser.py "$output_file" "$final_file" "$source_type" 54 | ruff check --fix "$final_file" 55 | ruff format "$final_file" 56 | cp "$final_file" "./harborapi/models/$(basename "$final_file")" 57 | -------------------------------------------------------------------------------- /docs/endpoints/artifacts.md: -------------------------------------------------------------------------------- 1 | # Artifacts 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_artifact 7 | - copy_artifact 8 | - delete_artifact 9 | - get_artifact_vulnerability_reports 10 | - get_artifact_build_history 11 | - get_artifact_accessories 12 | - get_artifact_tags 13 | - create_artifact_tag 14 | - delete_artifact_tag 15 | - add_artifact_label 16 | - delete_artifact_label 17 | - get_artifacts 18 | - get_artifact_vulnerabilities 19 | -------------------------------------------------------------------------------- /docs/endpoints/auditlogs.md: -------------------------------------------------------------------------------- 1 | # Audit Logs 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_audit_logs 7 | -------------------------------------------------------------------------------- /docs/endpoints/configure.md: -------------------------------------------------------------------------------- 1 | # Configure 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_config 7 | - update_config 8 | -------------------------------------------------------------------------------- /docs/endpoints/cveallowlist.md: -------------------------------------------------------------------------------- 1 | # CVE Allowlist 2 | 3 | Manage the system CVE allowlist. 4 | 5 | ::: harborapi.client.HarborAsyncClient 6 | options: 7 | members: 8 | - get_cve_allowlist 9 | - update_cve_allowlist 10 | -------------------------------------------------------------------------------- /docs/endpoints/gc.md: -------------------------------------------------------------------------------- 1 | # Garbage Collection 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_gc_schedule 7 | - create_gc_schedule 8 | - update_gc_schedule 9 | - get_gc_jobs 10 | - get_gc_job 11 | - get_gc_log 12 | -------------------------------------------------------------------------------- /docs/endpoints/health.md: -------------------------------------------------------------------------------- 1 | # Health Check 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - health_check 7 | -------------------------------------------------------------------------------- /docs/endpoints/icon.md: -------------------------------------------------------------------------------- 1 | # Icon 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_icon 7 | -------------------------------------------------------------------------------- /docs/endpoints/immutable.md: -------------------------------------------------------------------------------- 1 | # Immutable 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_project_immutable_tag_rules 7 | - create_project_immutable_tag_rule 8 | - update_project_immutable_tag_rule 9 | - enable_project_immutable_tagrule 10 | - delete_project_immutable_tag_rule 11 | -------------------------------------------------------------------------------- /docs/endpoints/index.md: -------------------------------------------------------------------------------- 1 | # Endpoints Overview 2 | 3 | This section contains API documentation for the methods implementing the different Harbor API endpoints. 4 | 5 | The methods listed can be called on an instance of the [`HarborAsyncClient`][harborapi.client.HarborAsyncClient] class: 6 | 7 | ```py 8 | from harborapi import HarborAsyncClient 9 | 10 | client = HarborAsyncClient(...) 11 | 12 | async def main() -> None: 13 | projects = await client.get_projects() 14 | repos = await client.get_repositories() 15 | 16 | # Resource creation requires a model object 17 | from harborapi.models import ProjectReq 18 | await client.create_project( 19 | ProjectReq( 20 | project_name="my-project", 21 | public=False, 22 | ), 23 | ) 24 | 25 | # etc... 26 | ``` 27 | 28 | See [Recipes](../recipes/index.md) for more examples on how to use the methods. 29 | 30 | ## Implemented Endpoints 31 | 32 | Check the [GitHub README](https://github.com/unioslo/harborapi/blob/main/README.md) for the most up to date overview of the implemented endpoints. 33 | -------------------------------------------------------------------------------- /docs/endpoints/labels.md: -------------------------------------------------------------------------------- 1 | # Labels 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_label 7 | - create_label 8 | - delete_label 9 | - get_labels 10 | -------------------------------------------------------------------------------- /docs/endpoints/ldap.md: -------------------------------------------------------------------------------- 1 | # LDAP 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - ping_ldap 7 | - search_ldap_groups 8 | - search_ldap_users 9 | - import_ldap_users 10 | -------------------------------------------------------------------------------- /docs/endpoints/oidc.md: -------------------------------------------------------------------------------- 1 | # OIDC 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - test_oidc 7 | -------------------------------------------------------------------------------- /docs/endpoints/permissions.md: -------------------------------------------------------------------------------- 1 | # Permissions 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_permissions 7 | -------------------------------------------------------------------------------- /docs/endpoints/ping.md: -------------------------------------------------------------------------------- 1 | # Ping 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - ping 7 | -------------------------------------------------------------------------------- /docs/endpoints/projectmetadata.md: -------------------------------------------------------------------------------- 1 | # Project Metadata 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_project_metadata 7 | - set_project_metadata 8 | - get_project_metadata_entry 9 | - update_project_metadata_entry 10 | - delete_project_metadata_entry 11 | -------------------------------------------------------------------------------- /docs/endpoints/projects.md: -------------------------------------------------------------------------------- 1 | # Projects 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_project 7 | - create_project 8 | - update_project 9 | - delete_project 10 | - get_projects 11 | - get_project_logs 12 | - get_project_summary 13 | - project_exists 14 | - set_project_scanner 15 | - get_project_scanner 16 | - get_project_scanner_candidates 17 | - get_project_deletable 18 | - get_project_member 19 | - add_project_member 20 | - add_project_member_user 21 | - add_project_member_group 22 | - update_project_member_role 23 | - remove_project_member 24 | - get_project_members 25 | -------------------------------------------------------------------------------- /docs/endpoints/purge.md: -------------------------------------------------------------------------------- 1 | # Purge 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_purge_job 7 | - stop_purge_job 8 | - get_purge_job_log 9 | - get_purge_job_schedule 10 | - create_purge_job_schedule 11 | - update_purge_job_schedule 12 | - get_purge_job_history 13 | -------------------------------------------------------------------------------- /docs/endpoints/quota.md: -------------------------------------------------------------------------------- 1 | # Quota 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_quota 7 | - update_quota 8 | - get_quotas 9 | -------------------------------------------------------------------------------- /docs/endpoints/registries.md: -------------------------------------------------------------------------------- 1 | # Registries 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_registry 7 | - create_registry 8 | - update_registry 9 | - delete_registry 10 | - get_registry_info 11 | - get_registry_adapters 12 | - get_registry_providers 13 | - check_registry_status 14 | - get_registries 15 | -------------------------------------------------------------------------------- /docs/endpoints/replication.md: -------------------------------------------------------------------------------- 1 | # Replication 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_replication 7 | - start_replication 8 | - stop_replication 9 | - get_replications 10 | - get_replication_tasks 11 | - get_replication_task_log 12 | - get_replication_policy 13 | - create_replication_policy 14 | - update_replication_policy 15 | - delete_replication_policy 16 | - get_replication_policies 17 | -------------------------------------------------------------------------------- /docs/endpoints/repositories.md: -------------------------------------------------------------------------------- 1 | # Repositories 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_repository 7 | - update_repository 8 | - delete_repository 9 | - get_repositories 10 | -------------------------------------------------------------------------------- /docs/endpoints/retention.md: -------------------------------------------------------------------------------- 1 | # Retention 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_project_retention_id 7 | - get_retention_policy 8 | - create_retention_policy 9 | - update_retention_policy 10 | - delete_retention_policy 11 | - get_retention_tasks 12 | - get_retention_metadata 13 | - get_retention_execution_task_log 14 | - get_retention_executions 15 | - start_retention_execution 16 | - stop_retention_execution 17 | -------------------------------------------------------------------------------- /docs/endpoints/robots.md: -------------------------------------------------------------------------------- 1 | # Robots 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_robot 7 | - create_robot 8 | - update_robot 9 | - refresh_robot_secret 10 | - delete_robot 11 | - get_robots 12 | -------------------------------------------------------------------------------- /docs/endpoints/robotv1.md: -------------------------------------------------------------------------------- 1 | # Robot V1 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_robots_v1 7 | - create_robot_v1 8 | - get_robot_v1 9 | - update_robot_v1 10 | - delete_robot_v1 11 | -------------------------------------------------------------------------------- /docs/endpoints/scan.md: -------------------------------------------------------------------------------- 1 | # Scan 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - scan_artifact 7 | - stop_artifact_scan 8 | - get_artifact_scan_report_log 9 | -------------------------------------------------------------------------------- /docs/endpoints/scanall.md: -------------------------------------------------------------------------------- 1 | # Scan All 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_scan_all_schedule 7 | - create_scan_all_schedule 8 | - update_scan_all_schedule 9 | - get_scan_all_metrics 10 | - stop_scan_all_job 11 | -------------------------------------------------------------------------------- /docs/endpoints/scanexport.md: -------------------------------------------------------------------------------- 1 | # Scan Data Export 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_scan_export 7 | - get_scan_exports 8 | - export_scan_data 9 | - download_scan_export 10 | -------------------------------------------------------------------------------- /docs/endpoints/scanners.md: -------------------------------------------------------------------------------- 1 | # Scanners 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_scanner 7 | - create_scanner 8 | - update_scanner 9 | - delete_scanner 10 | - get_scanners 11 | - set_default_scanner 12 | - ping_scanner_adapter 13 | -------------------------------------------------------------------------------- /docs/endpoints/search.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - search 7 | -------------------------------------------------------------------------------- /docs/endpoints/statistics.md: -------------------------------------------------------------------------------- 1 | # Statistics 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_statistics 7 | -------------------------------------------------------------------------------- /docs/endpoints/systeminfo.md: -------------------------------------------------------------------------------- 1 | # System Info 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_system_info 7 | - get_system_volume_info 8 | - get_system_certificate 9 | -------------------------------------------------------------------------------- /docs/endpoints/usergroups.md: -------------------------------------------------------------------------------- 1 | # User Groups 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_usergroup 7 | - create_usergroup 8 | - update_usergroup 9 | - delete_usergroup 10 | - get_usergroups 11 | - search_usergroups 12 | -------------------------------------------------------------------------------- /docs/endpoints/users.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_user 7 | - get_user_by_username 8 | - create_user 9 | - update_user 10 | - delete_user 11 | - get_users 12 | - search_users_by_username 13 | - set_user_admin 14 | - set_user_password 15 | - set_user_cli_secret 16 | - get_current_user 17 | - get_current_user_permissions 18 | -------------------------------------------------------------------------------- /docs/endpoints/webhooks.md: -------------------------------------------------------------------------------- 1 | # Webhooks 2 | 3 | ::: harborapi.client.HarborAsyncClient 4 | options: 5 | members: 6 | - get_webhook_jobs 7 | - get_webhook_policies 8 | - get_webhook_policy 9 | - create_webhook_policy 10 | - update_webhook_policy 11 | - delete_webhook_policy 12 | - get_webhook_policy_last_trigger 13 | - get_webhook_supported_events 14 | -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete0.png -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete1.png -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete2.png -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete3.png -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete4.png -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete5.png -------------------------------------------------------------------------------- /docs/img/usage/models/autocomplete6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/docs/img/usage/models/autocomplete6.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `harborapi` is a Python client for the Harbor REST API v2.0. 4 | 5 | ## Features 6 | 7 | - Async API 8 | - Fully typed 9 | - Data validation with [Pydantic](https://pydantic-docs.helpmanual.io/) 10 | - HTTP handled by [HTTPX](https://www.python-httpx.org/) 11 | - Extensive test coverage powered by [Hypothesis](https://hypothesis.works/) 12 | 13 | ## Installation 14 | 15 | ```bash 16 | pip install harborapi 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/recipes/artifacts/delete-artifact.md: -------------------------------------------------------------------------------- 1 | # Delete artifact 2 | 3 | We can delete an artifact using [`delete_artifact`][harborapi.client.HarborAsyncClient.delete_artifact]. The method takes a project name, repository name, and a tag or digest. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | 12 | async def main() -> None: 13 | await client.delete_artifact("library", "hello-world", "latest") 14 | # or 15 | await client.delete_artifact("library", "hello-world", "sha256:123456abcdef...") 16 | 17 | 18 | asyncio.run(main()) 19 | 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/recipes/artifacts/get-artifact-scan-overview.md: -------------------------------------------------------------------------------- 1 | # Get artifact scan overview 2 | 3 | We can fetch the scan overview for an artifact using the `with_scan_overview` argument. This will include a brief overview of the scan results for the artifact. To fetch the full vulnerability report (a separate API call), see the [Get artifact vulnerability report](get-artifact-vulnerabilities.md) recipe. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | async def main() -> None: 12 | artifact = await client.get_artifact( 13 | "library", 14 | "hello-world", 15 | "latest", 16 | with_scan_overview=True, 17 | ) 18 | print(artifact.scan_overview) 19 | 20 | 21 | asyncio.run(main()) 22 | ``` 23 | 24 | This will populate the [`scan_overview`][harborapi.models.Artifact.scan_overview] field of the artifact with a mapping of the different scan overviews available for the artifact, each key being a MIME-type and each value being a [`NativeReportSummary`][harborapi.models.NativeReportSummary] object. 25 | 26 | ```py 27 | print(list(artifact.scan_overview)) 28 | # ['application/vnd.security.vulnerability.report; version=1.1'] 29 | print(artifact.scan_overview["application/vnd.security.vulnerability.report; version=1.1"]) 30 | # NativeReportSummary(report_id="foo123", ...) 31 | ``` 32 | 33 | In almost every case, only a single scan overview will be available for the artifact. In those cases, we can use the `scan` attribute to access the first [`NativeReportSummary`][harborapi.models.NativeReportSummary] object found in the `scan_overview` mapping. 34 | 35 | ```py 36 | print(artifact.scan) 37 | # NativeReportSummary(report_id="foo123", ...) 38 | ``` 39 | 40 | Check the [`NativeReportSummary`][harborapi.models.NativeReportSummary] API reference for all available fields. For example, we can get the status, severity, and ID of the scan overview: 41 | 42 | ```py 43 | print("Status:", artifact.scan.scan_status) 44 | # 'Success' 45 | print("Severity:", artifact.scan.severity) 46 | # 'Critical' 47 | print("Report ID:", artifact.scan.report_id) 48 | # 'foo123' 49 | ``` 50 | 51 | 52 | The [`scan.summary`][harborapi.models.NativeReportSummary.summary] field is a [`VulnerabilitySummary`][harborapi.models.VulnerabilitySummary] object, which we can use to get a summary of the number of vulnerabilities found: 53 | 54 | ```py 55 | print("Critical:", artifact.scan.summary.critical) 56 | print("High:", artifact.scan.summary.high) 57 | print("Medium:", artifact.scan.summary.medium) 58 | print("Low:", artifact.scan.summary.low) 59 | print("Unknown:", artifact.scan.summary.unknown) 60 | print("Total:", artifact.scan.summary.total) 61 | print("Fixable:", artifact.scan.summary.critical) 62 | ``` 63 | 64 | ## Specific MIME type scan overview 65 | 66 | If we want to fetch the scan overview given a specific MIME-type, we can use the `mime_type` argument: 67 | 68 | ```py hl_lines="6" 69 | artifact = await client.get_artifact( 70 | "library", 71 | "hello-world", 72 | "latest", 73 | with_scan_overview=True, 74 | mime_type="application/vnd.security.vulnerability.report; version=1.1", 75 | ) 76 | scan = artifact.scan_overview["application/vnd.security.vulnerability.report; version=1.1"] 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/recipes/artifacts/get-artifacts.md: -------------------------------------------------------------------------------- 1 | # Get artifacts in repository 2 | 3 | To fetch all artifacts in a repository, we can use the [`get_artifacts`][harborapi.client.HarborAsyncClient.get_artifacts] method. It returns a list of [`Artifact`][harborapi.models.Artifact] objects. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | async def main() -> None: 12 | artifacts = await client.get_artifacts("library", "hello-world") 13 | 14 | 15 | asyncio.run(main()) 16 | ``` 17 | 18 | 19 | ## Filter by tag 20 | 21 | Providing an argument for `query` can help narrow down the results. For example, if we want to retrieve artifacts tagged `latest`, we can pass `"tags=latest"` to `query`: 22 | 23 | ```py hl_lines="4" 24 | artifacts = await client.get_artifacts( 25 | "project", 26 | "repository", 27 | query="tags=latest", 28 | ) 29 | ``` 30 | 31 | See [query](../../usage/methods/read.md#query) for more information about how to use this parameter. 32 | 33 | 34 | ## With extra data 35 | 36 | Similar to [`get_artifact`][harborapi.client.HarborAsyncClient.get_artifact], we can fetch extra data for the artifacts by using the `with_tag`, `with_label`, `with_scan_overview`, `with_signature`, `with_immutable_status`, `with_accessory` arguments. See the [get artifact](get-artifact.md) recipe for more information about how to use them and what they return. 37 | -------------------------------------------------------------------------------- /docs/recipes/client/reauth.md: -------------------------------------------------------------------------------- 1 | # Change client credentials 2 | 3 | To change the authentication credentials and/or API URL after the client has been instantiated, we can use the [`authenticate`][harborapi.HarborAsyncClient.authenticate] method: 4 | 5 | ```py 6 | from harborapi import HarborAsyncClient 7 | 8 | client = HarborAsyncClient( 9 | url="https://example.com/api/v2.0", 10 | username="user1", 11 | secret="user1pw", 12 | ) 13 | 14 | # Client uses API @ https://example.com as user1 15 | await client.get_projects() 16 | 17 | # NOTE: not async! 18 | client.authenticate( 19 | username="user2", 20 | secret="user2pw", 21 | url="https://demo.goharbor.io/api/v2.0", # optionally set a new url 22 | ) 23 | 24 | # Client uses API @ https://demo.goharbor.io as user2 25 | await client.get_projects() 26 | ``` 27 | 28 | We can also use it to only set a new URL: 29 | 30 | ```py 31 | client.authenticate(url="https://demo.goharbor.io/api/v2.0") 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/recipes/ext/artifact-vulns.md: -------------------------------------------------------------------------------- 1 | # Get vulnerabilities in all artifacts 2 | 3 | This recipe describes how to fetch all vulnerabilities from all artifacts in all repositories in all (or a subset of) projects using the helper functions defined in [`ext.api`](../../reference/ext/api.md). 4 | 5 | The recipe demonstrates how to fetch all artifacts that have vulnerabilities affecting OpenSSL version 3.x. It makes use of the built-in rate limiting implemented in [`get_artifact_vulnerabilities`][harborapi.ext.api.get_artifact_vulnerabilities]. By default, a maximum of 5 requests are sent concurrently, which prevents the program from accidentally performing a DoS attack on your Harbor instance. 6 | 7 | Attempting to fetch too many resources simultaneously can lead to extreme slowdowns and in some cases completely locking up your Harbor instance. Experiment with the `max_connections` argument of [`get_artifact_vulnerabilities`][harborapi.ext.api.get_artifact_vulnerabilities] to find the optimal value for your Harbor instance. 8 | 9 | 10 | 11 | ```py 12 | import asyncio 13 | from typing import Set 14 | 15 | from harborapi import HarborAsyncClient 16 | from harborapi.ext.api import get_artifact_vulnerabilities 17 | from harborapi.ext.artifact import ArtifactInfo 18 | from harborapi.ext.report import ArtifactReport 19 | 20 | client = HarborAsyncClient(...) 21 | 22 | 23 | async def main(): 24 | artifacts = await get_artifact_vulnerabilities( 25 | client, 26 | max_connections=5, 27 | ) 28 | 29 | # Aggregate the artifacts by making an ArtifactReport 30 | report = ArtifactReport(artifacts) 31 | 32 | # Filter report by only including artifacts with OpenSSL 3.x.y vulnerabilities 33 | filtered_report = report.with_package( 34 | "openssl", case_sensitive=False, min_version=(3, 0, 0) 35 | ) 36 | 37 | for artifact in filtered_report.artifacts: 38 | versions = get_all_openssl_versions(artifact) 39 | v = ", ".join(versions) # will likely just be 1 version 40 | print(f"{artifact.name_with_digest}: OpenSSL version: {v}") 41 | 42 | 43 | def get_all_openssl_versions(artifact: ArtifactInfo) -> Set[str]: 44 | """Get all affected OpenSSL versions for the artifact, 45 | with duplicates removed.""" 46 | return set( 47 | filter(None, [vuln.version for vuln in artifact.vulns_with_package("openssl")]) 48 | ) 49 | 50 | 51 | if __name__ == "__main__": 52 | asyncio.run(main()) 53 | ``` 54 | 55 | Example output: 56 | 57 | ```txt 58 | library/foo@sha256:f2f9fddc: OpenSSL version: 3.0.2-0ubuntu1.6 59 | other-project/bar@sha256:b498b376: OpenSSL version: 3.0.2-0ubuntu1.6 60 | legacy/baz@sha256:ddf6b9db: OpenSSL version: 3.0.2-0ubuntu1.6 61 | ``` 62 | 63 | In the example above, we make use of [`ArtifactReport.with_package`][harborapi.ext.report.ArtifactReport.with_package] to filter the report to only include artifacts with vulnerabilities affecting OpenSSL version 3.x. See the [ArtifactReport reference][harborapi.ext.report.ArtifactReport] for more information on other methods that can be used to filter the report. 64 | 65 | ## Fetching from a subset of projects 66 | 67 | In the example above we omitted the `projects` parameter for [`get_artifact_vulnerabilities`][harborapi.ext.api.get_artifact_vulnerabilities], which means that all projects will be queried. If you only want to query a subset of projects, you can pass a list of project names to the `projects` parameter. 68 | 69 | ```py hl_lines="3" 70 | artifacts = await get_artifact_vulnerabilities( 71 | client, 72 | projects=["library", "other-project"], 73 | max_connections=5, 74 | ) 75 | ``` 76 | -------------------------------------------------------------------------------- /docs/recipes/ext/artifactowner.md: -------------------------------------------------------------------------------- 1 | # Get artifact owner 2 | 3 | We can fetch information about owners of artifacts using [`harborapi.ext.api.get_artifact_owner`][harborapi.ext.api.get_artifact_owner]. The function takes in a [`harborapi.models.Artifact`][harborapi.models.Artifact] or [`harborapi.ext.artifact.ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] object, and returns a [`harborapi.models.UserResp`][harborapi.models.UserResp] object. 4 | 5 | 6 | !!! warning 7 | The method requires elevated privileges, as it has to look up information about users. A lack of privileges will likely result in [`harborapi.exceptions.Unauthorized`][harborapi.exceptions.Unauthorized] being raised. 8 | 9 | ```py 10 | import asyncio 11 | 12 | from harborapi import HarborAsyncClient 13 | from harborapi.ext import api 14 | 15 | client = HarborAsyncClient(...) 16 | 17 | sync def main() -> None: 18 | artifacts = await api.get_artifacts(client, projects=["library"]) 19 | for artifact in artifacts: 20 | try: 21 | owner_info = await api.get_artifact_owner(client, artifact.artifact) 22 | except ValueError as e: 23 | # something is wrong with the artifact, and we can't fetch its owner 24 | print(e) 25 | else: 26 | print(owner_info) 27 | 28 | 29 | if __name__ == "__main__": 30 | asyncio.run(main()) 31 | ``` 32 | 33 | In the above example, we fetch all artifacts in the `library` project, and then fetch the owner information for each artifact. If the artifact is not owned by a user or does not belong to a project, the function will raise a `ValueError`. 34 | 35 | The function returns a [`UserResp`][harborapi.models.UserResp] object, which contains information about the owner of the artifact. 36 | 37 | See [api.get_artifacts][harborapi.ext.api.get_artifacts] and [api.get_artifact_owner][harborapi.ext.api.get_artifact_owner] for more information. 38 | -------------------------------------------------------------------------------- /docs/recipes/ext/conc-artifact.md: -------------------------------------------------------------------------------- 1 | # Fetch artifacts 2 | 3 | With the help of asyncio, we can fetch artifacts from multiple repositories concurrently. 4 | The number of concurrent connections can be controlled by the `max_connections` parameter for [harborapi.ext.api.get_artifacts][]. 5 | 6 | 7 | ## All artifacts 8 | 9 | By default, `get_artifacts()` will fetch all artifacts in all repositories in all projects. 10 | 11 | ```py 12 | from harborapi import HarborAsyncClient 13 | from harborapi.ext import api 14 | 15 | client = HarborAsyncClient(...) 16 | 17 | async def main() -> None: 18 | artifacts = await api.get_artifacts(client) 19 | ``` 20 | 21 | This will give us a list of [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] objects, which contain information about the artifact. 22 | 23 | ## Artifacts in specific projects 24 | 25 | Passing a list of project names to the `projects` argument will fetch artifacts from all repositories in the specified projects. 26 | 27 | ```py hl_lines="9" 28 | from harborapi import HarborAsyncClient 29 | from harborapi.ext import api 30 | 31 | client = HarborAsyncClient(...) 32 | 33 | async def main() -> None: 34 | artifacts = await api.get_artifacts( 35 | client, 36 | projects=["library", "my-project-1"], 37 | ) 38 | ``` 39 | 40 | This will fetch all artifacts in all repositories in the projects `library` and `my-project-1` concurrently. 41 | 42 | 43 | ## Artifacts in specific repos in specific projects 44 | 45 | 46 | We can specify names of projects and repositories to fetch artifacts from. 47 | 48 | ```py hl_lines="9-10" 49 | from harborapi import HarborAsyncClient 50 | from harborapi.ext import api 51 | 52 | client = HarborAsyncClient(...) 53 | 54 | async def main() -> None: 55 | artifacts = await api.get_artifacts( 56 | client, 57 | projects=["library", "test-project"], 58 | repositories=["nginx"] 59 | ) 60 | ``` 61 | 62 | This will fetch all artifacts in `library/nginx` and `test-project/nginx` concurrently (if they exist). 63 | 64 | 65 | 66 | ## Artifacts in specific repos 67 | 68 | 69 | We can fetch artifacts from specific repositories by passing a list of repository names to the `repositories` argument for [harborapi.ext.api.get_artifacts][]. 70 | 71 | 72 | ```py hl_lines="9" 73 | from harborapi import HarborAsyncClient 74 | from harborapi.ext import api 75 | 76 | client = HarborAsyncClient(...) 77 | 78 | async def main() -> None: 79 | artifacts = await api.get_artifacts( 80 | client, 81 | repositories=["library/hello-world", "nginx"] 82 | ) 83 | ``` 84 | 85 | This will fetch all artifacts in the repository `library/hello-world`, as well as all artifacts from any repository named `nginx` in any project. 86 | 87 | The `"nginx"` value demonstrates the flexible behavior of the function. By omitting both the `projects` parameter and the project name from the argument `"nginx"`, the library looks for repositories named `nginx` in all projects. 88 | -------------------------------------------------------------------------------- /docs/recipes/ext/conc-repo.md: -------------------------------------------------------------------------------- 1 | # Fetch repositories 2 | 3 | Given a list of project names, we can use Asyncio to dispatch multiple requests concurrently to the Harbor API to fetch repositories in a list of projects (or all projects if `None` is passed in) with the help of [`ext.api.get_repositories`][harborapi.ext.api.get_repositories] 4 | 5 | ## List of names 6 | 7 | We can use a list of project names to fetch repositories from. 8 | 9 | 10 | ```py hl_lines="13" 11 | from harborapi import HarborAsyncClient 12 | from harborapi.ext import api 13 | 14 | client = HarborAsyncClient(...) 15 | 16 | async def main() -> None: 17 | repos = await api.get_repositories( 18 | client, 19 | projects=["library", "my-project-1"], 20 | ) 21 | ``` 22 | 23 | This will fetch all repositories from the projects `library` and `my-project-1` concurrently. 24 | 25 | 26 | ## All projects 27 | 28 | We can also fetch the repositories from all projects by passing `None` in as the `projects` argument. 29 | 30 | ```py hl_lines="13" 31 | from harborapi import HarborAsyncClient 32 | from harborapi.ext import api 33 | 34 | client = HarborAsyncClient(...) 35 | 36 | async def main() -> None: 37 | repos = await api.get_repositories( 38 | client, 39 | projects=None, 40 | ) 41 | ``` 42 | 43 | This will fetch all repositories from all projects concurrently. 44 | 45 | !!! note 46 | The function has a named parameter [`callback`][harborapi.ext.api.get_repositories], which takes a function that receives a list of exceptions as its only argument. This can be used to handle exceptions that occur during the concurrent requests. The function always fires even if there are no exceptions. If no callback function is specified, exceptions are ignored. 47 | -------------------------------------------------------------------------------- /docs/recipes/ext/index.md: -------------------------------------------------------------------------------- 1 | # `harborapi.ext` 2 | 3 | The `harborapi.ext` module provides additional functionality for common tasks such as concurrency and aggregation of multiple API calls. The recipes in this section make use of this module. 4 | 5 | These recipes demonstrate how to use some of these extra features. 6 | 7 | 8 | -------------------------------------------------------------------------------- /docs/recipes/index.md: -------------------------------------------------------------------------------- 1 | # Recipes 2 | 3 | This section contains recipes for common tasks across several Harbor API endpoints. They demonstrate how to create, read, update and delete resources. 4 | 5 | If a specific method is not documented in a recipe here, you can likely deduce how to use it by checking a recipe that performs a similar task and looking at the documentation for the method in the [Endpoints Overview](../endpoints/index.md). 6 | -------------------------------------------------------------------------------- /docs/recipes/projects/create-project.md: -------------------------------------------------------------------------------- 1 | # Create project 2 | 3 | We can create a new project using [`create_project`][harborapi.client.HarborAsyncClient.create_project]. The method takes a [`ProjectReq`][harborapi.models.ProjectReq] object, and returns the location of the created project. 4 | 5 | One feature of creating projects via the API is that we can provide more detailed configuration than what is available in the web UI. For example, we can enable content trust and auto scanning when creating the project, instead of having to do it manually after the project has been created. 6 | 7 | Check the [`ProjectReq`][harborapi.models.ProjectReq] documentation for more information about the available options. 8 | 9 | 10 | ```py 11 | import asyncio 12 | from harborapi import HarborAsyncClient 13 | 14 | client = HarborAsyncClient(...) 15 | 16 | 17 | async def main() -> None: 18 | location = await client.create_project( 19 | ProjectReq( 20 | project_name="new-project", 21 | public=True, 22 | metadata=ProjectMetadata( 23 | auto_scan=True, 24 | enable_content_trust=True, 25 | ), 26 | ) 27 | ) 28 | print(location) 29 | 30 | 31 | asyncio.run(main()) 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/recipes/projects/delete-project.md: -------------------------------------------------------------------------------- 1 | # Delete project 2 | 3 | We can delete a project using [`delete_project`][harborapi.client.HarborAsyncClient.delete_project]. The method takes a project name (string) or a project ID (integer) as its only argument. The method returns nothing on success. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | 12 | async def main() -> None: 13 | await client.delete_project("library") 14 | # or 15 | await client.delete_project(1) 16 | 17 | 18 | asyncio.run(main()) 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/recipes/projects/get-project.md: -------------------------------------------------------------------------------- 1 | # Get project 2 | 3 | We can fetch a single project using [`get_project`][harborapi.client.HarborAsyncClient.get_project]. The method takes a project name (string) or a project ID (integer) as its only argument. The method returns a [`Project`][harborapi.models.Project] object. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | 12 | async def main() -> None: 13 | project = await client.get_project("library") 14 | # or 15 | project = await client.get_project(1) 16 | 17 | 18 | asyncio.run(main()) 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/recipes/projects/get-projects.md: -------------------------------------------------------------------------------- 1 | # Get all projects 2 | 3 | We can fetch all projects using [`get_projects`][harborapi.client.HarborAsyncClient.get_projects]. The method returns a list of [`Project`][harborapi.models.Project] objects. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | 12 | async def main() -> None: 13 | projects = await client.get_projects() 14 | 15 | 16 | asyncio.run(main()) 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/recipes/projects/update-project.md: -------------------------------------------------------------------------------- 1 | # Update project 2 | 3 | We can update an existing project using [`update_project`][harborapi.client.HarborAsyncClient.update_project]. The method takes the name or ID of the project and a [`ProjectReq`][harborapi.models.ProjectReq] object. Nothing is returned on success. 4 | 5 | !!! note 6 | Updating a project is an `HTTP PUT` operation in the API, which according to idiomatic REST should replace the existing project settings with the project settings in the `ProjectReq` in the request body. However, in practice, the Harbor API supports partial updates, and thus will only update the fields that are actually set on the `ProjectReq` object in the request body. It is not guaranteed that this behavior will persist in future versions of Harbor, or is indeed supported by all versions of Harbor. 7 | 8 | See [Idiomatic REST Updating](../../usage/methods/create-update.md/#idiomatic-rest-updating) for more information. 9 | 10 | ```py 11 | import asyncio 12 | from harborapi import HarborAsyncClient 13 | 14 | client = HarborAsyncClient(...) 15 | 16 | 17 | async def main() -> None: 18 | await client.update_project( 19 | "test-project2", 20 | ProjectReq( 21 | public=False, 22 | metadata=ProjectMetadata( 23 | auto_scan=False, 24 | ), 25 | ), 26 | ) 27 | 28 | 29 | asyncio.run(main()) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/recipes/repos/get-repo.md: -------------------------------------------------------------------------------- 1 | # Get repository 2 | 3 | We can fetch a specific repository using [`get_repository`][harborapi.client.HarborAsyncClient.get_repository]. The method takes a project name and a repository name, and returns a [`Repository`][harborapi.models.Repository] object. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | async def main() -> None: 12 | repo = await client.get_repository( 13 | project_name="library", 14 | repository_name="hello-world", 15 | ) 16 | 17 | 18 | asyncio.run(main()) 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/recipes/repos/get-repos-project.md: -------------------------------------------------------------------------------- 1 | # Get repositories in a project 2 | 3 | We can fetch all repositories in a specific project by using [`get_repositories`][harborapi.client.HarborAsyncClient.get_repositories] and passing the project name to the `project_name` parameter. The method returns a list of [`Repository`][harborapi.models.Repository] objects. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | async def main() -> None: 12 | repos = await client.get_repositories( 13 | project_name="library", 14 | ) 15 | 16 | 17 | asyncio.run(main()) 18 | ``` 19 | 20 | Fetching repos in multiple specific projects must either be done by calling the method multiple times, or omitting the `project_name` parameter and fetching all repositories in all projects, and then filtering the results afterwards. 21 | 22 | `harborapi.ext` provides a helper function for fetching from multiple specific projects, and the recipe for that is available [here](../ext/conc-repo.md) 23 | -------------------------------------------------------------------------------- /docs/recipes/repos/get-repos.md: -------------------------------------------------------------------------------- 1 | # Get all repositories 2 | 3 | Fetching all repositories is done by calling [`get_repositories`][harborapi.client.HarborAsyncClient.get_repositories]. The method returns a list of [`Repository`][harborapi.models.Repository] objects. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | 12 | async def main() -> None: 13 | repos = await client.get_repositories() 14 | 15 | 16 | asyncio.run(main()) 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/recipes/retention/get-retention.md: -------------------------------------------------------------------------------- 1 | # Get project retention policy 2 | 3 | Fetching the retention policy for a given project is somewhat convoluted in the API, as there is no endpoint that directly returns the retention policy for a given project. Normally, we would have to fetch the project, inspect its metadata, and then fetch the retention policy using the ID from the metadata. 4 | 5 | `harborapi` adds the helper method [`get_project_retention_id`][harborapi.client.HarborAsyncClient.get_project_retention_id] for fetching the retention policy ID for a given project. With this method, we can first fetch the retention policy ID, and then use that ID to fetch the retention policy itself. 6 | 7 | 8 | ```py 9 | import asyncio 10 | 11 | from harborapi import HarborAsyncClient 12 | 13 | client = HarborAsyncClient(...) 14 | 15 | 16 | async def main() -> None: 17 | # Get the retention policy ID for the project "library" 18 | project_name = "library" 19 | retention_id = await client.get_project_retention_id(project_name) 20 | if not retention_id: 21 | print(f"No retention policy found for project {project_name!r}") 22 | exit(1) 23 | 24 | # Get the retention policy for the project "library" 25 | policy = await client.get_retention_policy(retention_id) 26 | 27 | # work with the policy... 28 | print(policy) 29 | 30 | 31 | asyncio.run(main()) 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/recipes/scan/scan-artifact.md: -------------------------------------------------------------------------------- 1 | # Scan an artifact 2 | 3 | We can scan an artifact using [`scan_artifact`][harborapi.client.HarborAsyncClient.scan_artifact]. The method takes a project name, repository name, and a tag or digest. The method starts a scan and returns nothing on success. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | async def main() -> None: 12 | await client.scan_artifact("library", "hello-world", "latest") 13 | # or 14 | await client.scan_artifact("library", "hello-world", "sha256:123456abcdef...") 15 | 16 | 17 | 18 | asyncio.run(main()) 19 | ``` 20 | 21 | 22 | ## Stop a scan 23 | 24 | We can stop a running scan by using [`stop_artifact_scan`][harborapi.client.HarborAsyncClient.stop_artifact_scan]. The method takes a project name, repository name, and a tag or digest. The method returns nothing on success. 25 | 26 | ```py 27 | import asyncio 28 | from harborapi import HarborAsyncClient 29 | 30 | client = HarborAsyncClient(...) 31 | 32 | async def main() -> None: 33 | await client.stop_artifact_scan("library", "hello-world", "latest") 34 | # or 35 | await client.stop_artifact_scan("library", "hello-world", "sha256:123456abcdef...") 36 | 37 | 38 | asyncio.run(main()) 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/recipes/user/get-current-user.md: -------------------------------------------------------------------------------- 1 | ## Get current user 2 | 3 | To fetch information about the currently authenticated API user, we can use the [`get_current_user`][harborapi.client.HarborAsyncClient.get_current_user] method. It returns a [`UserResp`][harborapi.models.UserResp] object. 4 | 5 | 6 | ```py 7 | import asyncio 8 | 9 | from harborapi import HarborAsyncClient 10 | 11 | client = HarborAsyncClient(...) 12 | 13 | 14 | async def main(): 15 | res = await client.get_current_user() 16 | print(res) 17 | 18 | 19 | asyncio.run(main()) 20 | ``` 21 | 22 | Produces something like this: 23 | 24 | ```py 25 | UserResp( 26 | email=None, 27 | realname='Firstname Lastname', 28 | comment='from LDAP.', 29 | user_id=123, 30 | username='firstname-lastname', 31 | sysadmin_flag=False, 32 | admin_role_in_auth=True, 33 | oidc_user_meta=None, 34 | creation_time=datetime.datetime(2022, 7, 1, 13, 19, 36, 26000, tzinfo=datetime.timezone.utc), 35 | update_time=datetime.datetime(2022, 7, 1, 13, 19, 36, 26000, tzinfo=datetime.timezone.utc) 36 | ) 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/reference/auth.md: -------------------------------------------------------------------------------- 1 | # harborapi.auth 2 | 3 | `harborapi.auth` defines utility functions for use with `harborapi`. 4 | 5 | ::: harborapi.auth 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: false 9 | show_source: true 10 | show_bases: true 11 | -------------------------------------------------------------------------------- /docs/reference/client.md: -------------------------------------------------------------------------------- 1 | # harborapi.client 2 | 3 | See [Endpoints](../endpoints/index.md) for all methods implemented on the class used to interact with the Harbor API. 4 | 5 | ::: harborapi.client 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: true 9 | show_source: true 10 | show_bases: true 11 | members: 12 | - HarborAsyncClient 13 | -------------------------------------------------------------------------------- /docs/reference/client_sync.md: -------------------------------------------------------------------------------- 1 | # harborapi.client_sync 2 | 3 | ::: harborapi.client_sync 4 | options: 5 | merge_init_into_class: false 6 | show_if_no_docstring: false 7 | show_source: true 8 | show_bases: true 9 | -------------------------------------------------------------------------------- /docs/reference/exceptions.md: -------------------------------------------------------------------------------- 1 | # harborapi.exceptions 2 | 3 | ::: harborapi.exceptions 4 | options: 5 | merge_init_into_class: true 6 | show_if_no_docstring: true 7 | show_source: true 8 | show_bases: true 9 | -------------------------------------------------------------------------------- /docs/reference/ext/api.md: -------------------------------------------------------------------------------- 1 | # harborapi.ext.api 2 | 3 | The `harborapi.ext.api` module contains helper functions for performing common tasks with the Harbor API that are not included in the API spec, such as fetching all vulnerabilities for all artifacts in one or more projects or repositories, fetching all repositories in one or more projects, etc. These functions are implemented using the standard endpoint methods from `harborapi.HarborAsyncClient`. 4 | 5 | ::: harborapi.ext.api 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: true 9 | show_source: true 10 | show_bases: true 11 | 13 | -------------------------------------------------------------------------------- /docs/reference/ext/artifact.md: -------------------------------------------------------------------------------- 1 | # harborapi.ext.artifact 2 | 3 | Module that defines the `ArtifactInfo` class that aggregates information about an artifact from several Harbor API models. 4 | 5 | ::: harborapi.ext.artifact 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: false 9 | show_source: true 10 | show_bases: true 11 | 13 | -------------------------------------------------------------------------------- /docs/reference/ext/cve.md: -------------------------------------------------------------------------------- 1 | # harborapi.ext.cve 2 | 3 | The `harborapi.ext.cve` module provides functionality for working with CVSSv3 data. 4 | 5 | ::: harborapi.ext.cve 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: false 9 | show_source: true 10 | show_bases: true 11 | -------------------------------------------------------------------------------- /docs/reference/ext/report.md: -------------------------------------------------------------------------------- 1 | # harborapi.ext.report 2 | 3 | `harborapi.ext.report` defines classes and functions for aggregating the data of multiple artifacts, including their repositories and vulnerability reports. 4 | 5 | ::: harborapi.ext.report 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: false 9 | show_source: true 10 | show_bases: true 11 | 13 | -------------------------------------------------------------------------------- /docs/reference/index.md: -------------------------------------------------------------------------------- 1 | # Reference 2 | 3 | - [harborapi.auth](auth.md) 4 | - [harborapi.client](client.md) 5 | - [harborapi.client_sync](client_sync.md) 6 | - [harborapi.exceptions](exceptions.md) 7 | - [harborapi.types](types.md) 8 | - [harborapi.utils](utils.md) 9 | - [harborapi.models.scanner](models/scanner.md) 10 | - [harborapi.models.models](models/models.md) 11 | - [harborapi.ext.cve](ext/cve.md) 12 | - [harborapi.ext.report](ext/report.md) 13 | - [harborapi.ext.api](ext/api.md) 14 | - [harborapi.ext.artifact](ext/artifact.md) 15 | 16 | -------------------------------------------------------------------------------- /docs/reference/models/_models.md: -------------------------------------------------------------------------------- 1 | # harborapi.models._models 2 | 3 | These models have been generated from the official Harbor REST API [Swagger 2.0 Schema](https://github.com/goharbor/harbor/blob/main/api/v2.0/swagger.yaml) using [datamodel-code-generator](https://koxudaxi.github.io/datamodel-code-generator/) version 0.13.0. 4 | 5 | ::: harborapi.models._models 6 | options: 7 | show_if_no_docstring: true 8 | show_source: true 9 | show_bases: false 10 | -------------------------------------------------------------------------------- /docs/reference/models/_scanner.md: -------------------------------------------------------------------------------- 1 | # harborapi.models._scanner 2 | 3 | These models have been generated from the official Harbor Pluggable Scanner Spec [OpenAPI 3.0 Schema](https://github.com/goharbor/pluggable-scanner-spec/blob/master/api/spec/scanner-adapter-openapi-v1.1.yaml) using [datamodel-code-generator](https://koxudaxi.github.io/datamodel-code-generator/) version 0.13.0. 4 | 5 | ::: harborapi.models._scanner 6 | options: 7 | show_if_no_docstring: true 8 | show_source: true 9 | show_bases: false 10 | -------------------------------------------------------------------------------- /docs/reference/models/base.md: -------------------------------------------------------------------------------- 1 | # harborapi.models.base 2 | 3 | ::: harborapi.models.base 4 | options: 5 | show_if_no_docstring: true 6 | show_source: true 7 | show_bases: false 8 | -------------------------------------------------------------------------------- /docs/reference/models/buildhistory.md: -------------------------------------------------------------------------------- 1 | # harborapi.models.buildhistory 2 | 3 | Module that defines build history models. These are not generated from the Swagger 2.0 schema, but are instead defined manually. This is because the Swagger 2.0 schema does not define the OIDC models. 4 | 5 | ::: harborapi.models.buildhistory 6 | options: 7 | show_if_no_docstring: true 8 | show_source: true 9 | show_bases: false 10 | -------------------------------------------------------------------------------- /docs/reference/models/mappings.md: -------------------------------------------------------------------------------- 1 | # harborapi.models.mappings 2 | 3 | Custom mapping types. 4 | 5 | ::: harborapi.models.mappings 6 | options: 7 | show_if_no_docstring: true 8 | show_source: true 9 | show_bases: false 10 | -------------------------------------------------------------------------------- /docs/reference/models/models.md: -------------------------------------------------------------------------------- 1 | # harborapi.models.models 2 | 3 | The canonical representation of the Harbor API models, after modifications have been made to rectify inconsistencies and errors in the official Swagger schema. To see the unmodified auto-generated models, see the [harborapi.models._models](./_models.md) module. 4 | 5 | ::: harborapi.models.models 6 | options: 7 | show_if_no_docstring: true 8 | show_source: true 9 | show_bases: true 10 | show_submodules: true 11 | -------------------------------------------------------------------------------- /docs/reference/models/oidc.md: -------------------------------------------------------------------------------- 1 | # harborapi.models.oidc 2 | 3 | Module that defines OIDC models. These are not generated from the Swagger 2.0 schema, but are instead defined manually. This is because the Swagger 2.0 schema does not define the OIDC models. 4 | 5 | ::: harborapi.models.oidc 6 | options: 7 | show_if_no_docstring: true 8 | show_source: true 9 | show_bases: false 10 | -------------------------------------------------------------------------------- /docs/reference/models/scanner.md: -------------------------------------------------------------------------------- 1 | # harborapi.models.scanner 2 | 3 | ::: harborapi.models.scanner 4 | options: 5 | show_if_no_docstring: true 6 | show_source: true 7 | show_bases: true 8 | -------------------------------------------------------------------------------- /docs/reference/responselog.md: -------------------------------------------------------------------------------- 1 | # harborapi.responselog 2 | 3 | ::: harborapi.responselog.ResponseLog 4 | options: 5 | merge_init_into_class: false 6 | show_if_no_docstring: true 7 | show_source: true 8 | show_bases: true 9 | 10 | ::: harborapi.responselog.ResponseLogEntry 11 | options: 12 | merge_init_into_class: false 13 | show_if_no_docstring: true 14 | show_source: true 15 | show_bases: true 16 | -------------------------------------------------------------------------------- /docs/reference/retry.md: -------------------------------------------------------------------------------- 1 | # harborapi.retry 2 | 3 | ::: harborapi.retry 4 | options: 5 | merge_init_into_class: false 6 | show_if_no_docstring: true 7 | show_source: true 8 | show_bases: true 9 | 10 | ## Backoff types 11 | 12 | The following types come from the [backoff](https://github.com/litl/backoff) package. They are documented here for convenience. 13 | 14 | 15 | ::: backoff._typing 16 | options: 17 | heading_level: 3 18 | merge_init_into_class: true 19 | show_if_no_docstring: true 20 | show_source: true 21 | show_bases: true 22 | members: 23 | - _Details 24 | - Details 25 | - _CallableT 26 | - _Handler 27 | - _Jitterer 28 | - _MaybeCallable 29 | - _MaybeLogger 30 | - _MaybeSequence 31 | - _WaitGenerator 32 | -------------------------------------------------------------------------------- /docs/reference/types.md: -------------------------------------------------------------------------------- 1 | # harborapi.types 2 | 3 | `harborapi.types` defines custom types for use with `harborapi`. 4 | 5 | ::: harborapi._types 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: true 9 | show_source: true 10 | show_bases: true 11 | -------------------------------------------------------------------------------- /docs/reference/utils.md: -------------------------------------------------------------------------------- 1 | # harborapi.utils 2 | 3 | `harborapi.utils` defines utility functions for use with `harborapi`. 4 | 5 | ::: harborapi.utils 6 | options: 7 | merge_init_into_class: false 8 | show_if_no_docstring: false 9 | show_source: true 10 | show_bases: true 11 | -------------------------------------------------------------------------------- /docs/usage/async-sync.md: -------------------------------------------------------------------------------- 1 | # Async vs sync 2 | 3 | 4 | ## Async client 5 | 6 | `harborapi` is predominantly focused on providing an async API for interacting with the Harbor API. The various code snippets on these pages all assume the instantiated client is [`HarborAsyncClient`][harborapi.client.HarborAsyncClient], and it is running within an asynchronous function where `await` can be used. 7 | 8 | If you only intend to use the Async Client, skip this page. 9 | 10 | ## Non-async client 11 | 12 | `harborapi` also provides `HarborClient` as a non-async alternative. `HarborClient` provides all the same methods as `HarborAsyncClient`, except it schedules the asynchronous methods to run as coroutines in the event loop by intercepting attribute access on the class. 13 | 14 | All\* methods on `HarborClient` have the same interface as the methods on `HarborAsyncClient`, except `await` is not required. 15 | 16 | When using the non-async client [`HarborClient`][harborapi.HarborClient], all methods should be invoked identically to methods on [`HarborAsyncClient`][harborapi.client.HarborAsyncClient], except the `await` keyword in front of the method call must be omitted. 17 | 18 | ### Example 19 | 20 | ```py 21 | import asyncio 22 | from harborapi import HarborClient 23 | 24 | client = HarborClient( 25 | url="https://your-harbor-instance.com/api/v2.0", 26 | username="username", 27 | secret="secret", 28 | ) 29 | 30 | res = client.get_current_user() 31 | ``` 32 | 33 | It is not recommended to use this client, but is provided as an alternative if you _absolutely_ don't want to deal with anything related to `asyncio` yourself. Due to the metaprogramming involved in making this non-async client, autocompletion and type hints are not available for the various methods. 34 | 35 | --- 36 | 37 | \* Private methods (prefixed with `_`) and HTTP methods such as `get`, `post`, etc. cannot be called without `await`. 38 | -------------------------------------------------------------------------------- /docs/usage/authentication.md: -------------------------------------------------------------------------------- 1 | The client can be instatiated with either a username and password, a base64-encoded [HTTP Basic Access Authentication Token](https://en.wikipedia.org/wiki/Basic_access_authentication), or a Harbor JSON credentials file. 2 | 3 | ### Username and password 4 | 5 | Username and password (titled `secret` to conform with Harbor naming schemes) can be used by instantiating the client with the `username` and `secret` parameters. This is the most straight forward method of authenticating. 6 | 7 | ```py 8 | from harborapi import HarborAsyncClient 9 | 10 | client = HarborAsyncClient( 11 | url="https://your-harbor-instance.com/api/v2.0", 12 | username="username", 13 | secret="secret" 14 | ) 15 | ``` 16 | 17 | In order to avoid hard-coding secrets in your application, you might want to consider using environment variables to store the username and password: 18 | 19 | ```py 20 | import os 21 | from harborapi import HarborAsyncClient 22 | 23 | client = HarborAsyncClient( 24 | url="https://your-harbor-instance.com/api/v2.0", 25 | username=os.environ["HARBOR_USERNAME"], 26 | secret=os.environ["HARBOR_PASSWORD"] 27 | ) 28 | ``` 29 | 30 | ### Basic access authentication aredentials 31 | 32 | In place of `username` and `secret`, a Base64-encoded [HTTP Basic Access Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) credentials string can be used to authenticate. 33 | This string is simply `username:secret` encoded to Base64, and as such is not any more secure than username and password authentication; it only obscures the text. 34 | 35 | ```py 36 | from harborapi import HarborAsyncClient 37 | 38 | client = HarborAsyncClient( 39 | url="https://your-harbor-instance.com/api/v2.0", 40 | basicauth="base64_basicauth_here", 41 | ) 42 | ``` 43 | 44 | Again, it might be pertinent to store this in your environment variables: 45 | 46 | ```py 47 | import os 48 | from harborapi import HarborAsyncClient 49 | 50 | client = HarborAsyncClient( 51 | url="https://your-harbor-instance.com/api/v2.0", 52 | basicauth=os.environ["HARBOR_BASICAUTH"], 53 | ) 54 | ``` 55 | 56 | ### Credentials file 57 | 58 | When [creating Robot accounts](https://goharbor.io/docs/1.10/working-with-projects/project-configuration/create-robot-accounts/), the robot account's credentials can be exported as a JSON file. The `credentials_file` parameter takes an argument specifying the path to such a file. 59 | 60 | 61 | ```py 62 | from harborapi import HarborAsyncClient 63 | 64 | client = HarborAsyncClient( 65 | url="https://your-harbor-instance.com/api/v2.0", 66 | credentials_file="/path/to/file.json", # can also be Path object 67 | ) 68 | ``` 69 | 70 | For simple project-level robot accounts, using the _Robot Accounts_ tab in the web interface for a project should be sufficient. However, if you require a Robot account with privileges that go beyond the ones offered in the Web UI, such as controlling user groups and replication, managing multiple projects, starting scans, or managing the system configuration, you will need to create a system-level Robot account through the API. See [Creating Privileged Robot Accounts](creating-system-robot.md) for information about how to create system-level Robot accounts with such extended privileges using `harborapi`. 71 | -------------------------------------------------------------------------------- /docs/usage/creating-system-robot.md: -------------------------------------------------------------------------------- 1 | # Privileged robot accounts 2 | 3 | By default, the Robot account creation process in the Harbor web interface only allows for a limited permission scope when creating new Robot accounts. As of Harbor v.2.5.2, this is still the case. 4 | 5 | In order to circumvent this limitation, one can create robot accounts through the API with system resource permissions that go beyond the options offered in the web interface. 6 | 7 | !!! info 8 | This page is based on [this](https://github.com/goharbor/harbor/issues/14145#issuecomment-781006533) comment by Harbor developer [wy65701436](https://github.com/wy65701436). Also check out the Harbor [source code](https://github.com/goharbor/harbor/blob/main/src/common/rbac/const.go) for more information on all the possible resource permissions that can be granted to Robot accounts. 9 | 10 | ## Example: Robot with project creation privileges 11 | 12 | Following the example provided in the GitHub comment above, `harborapi` provides [`HarborAsyncClient.create_robot`][harborapi.client.HarborAsyncClient.create_robot] to achieve the same functionality. 13 | 14 | When creating privileged robot accounts, `HarborAsyncClient` must be instantiated with a privileged user's credentials. 15 | 16 | ```py 17 | from harborapi.models import RobotCreate, RobotPermission, Access 18 | 19 | # Client is instantiated with administrator account. 20 | # This is required to create robot accounts with system resource permissions. 21 | 22 | await client.create_robot( 23 | RobotCreate( 24 | name="from_api", 25 | description="Created from harborapi Python client", 26 | secret="Secret1234", 27 | level="system", 28 | duration=30, 29 | permissions=[ 30 | RobotPermission( 31 | kind="system", 32 | namespace="/", 33 | access=[ 34 | Access(resource="project", action="create"), 35 | ], 36 | ) 37 | ], 38 | ) 39 | ) 40 | ``` 41 | 42 | Produces: 43 | 44 | ```py 45 | RobotCreated( 46 | id=11, 47 | name='robot$from_api', 48 | secret='tBuDZ700tPkKLNQ0z1EAYndMOFEzvgM8', 49 | creation_time=datetime.datetime(2022, 7, 14, 10, 3, 40, 906000, tzinfo=datetime.timezone.utc), 50 | expires_at=1660385020, 51 | ) 52 | ``` 53 | 54 | ### Saving credentials to a file 55 | 56 | The resulting Robot account can be saved to a Harbor credentials file by providing an argument to the `path` parameter specifying the location to save the credentials to. The `path` argument can be either a string or a [pathlib.Path][] object. 57 | 58 | ```py hl_lines="3" 59 | await client.create_robot( 60 | RobotCreate(...), 61 | path="/path/to/file.json", 62 | ) 63 | ``` 64 | 65 | By default, the file must not already exist. This can be overriden by adding `overwrite=True`, which overwrites the file if it already exists. 66 | 67 | ```py hl_lines="4" 68 | await client.create_robot( 69 | RobotCreate(...), 70 | path="/path/to/file.json", 71 | overwrite=True 72 | ) 73 | ``` 74 | 75 | The resulting file can then be used when instantiating `HarborAsyncClient` to authenticate with the Robot account. 76 | 77 | ```py hl_lines="5" 78 | from harborapi import HarborAsyncClient 79 | 80 | client = HarborAsyncClient( 81 | url="https://your-harbor-instance.com/api/v2.0", 82 | credentials_file="/path/to/file.json", 83 | ) 84 | ``` 85 | 86 | 87 | For more information, see [`HarborAsyncClient.create_robot`][harborapi.client.HarborAsyncClient.create_robot] and [`HarborAsyncClient.__init__`][harborapi.HarborAsyncClient.__init__] 88 | -------------------------------------------------------------------------------- /docs/usage/exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | All methods that interact with the Harbor API raise exceptions derived from [`harborapi.exceptions.StatusError`][harborapi.exceptions.StatusError] for responses with non-2xx status codes unless otherwise specified. 4 | 5 | ## Response 6 | 7 | Each exception contains the response that caused the exception to be raised, and the status code of the response. The response object is a [`httpx.Response`](https://www.python-httpx.org/api/#response) object. 8 | 9 | ```py 10 | try: 11 | await client.delete_artifact("project", "repository", "latest") 12 | except StatusError as e: 13 | print(e.response) 14 | print(e.status_code) # or e.response.status_code 15 | ``` 16 | 17 | Through the response object, we can also get the corresponding [`httpx.Request`](https://www.python-httpx.org/api/#request) through the `request` attribute. 18 | 19 | ```py 20 | e.response.request 21 | ``` 22 | 23 | ## Granular exception handling 24 | 25 | If more granular exception handling is required, all documented HTTP exceptions in the API spec are implemented as discrete subclasses of [`harborapi.exceptions.StatusError`][harborapi.exceptions.StatusError]. 26 | 27 | ```py 28 | from harborapi.exceptions import ( 29 | BadRequest, 30 | Forbidden, 31 | NotFound, 32 | MethodNotAllowed, 33 | Conflict, 34 | Unauthorized, 35 | PreconditionFailed, 36 | UnsupportedMediaType, 37 | InternalServerError, 38 | StatusError 39 | ) 40 | 41 | project, repo, tag = "testproj", "testrepo", "latest" 42 | 43 | try: 44 | await client.delete_artifact(project, repo, tag) 45 | except NotFound as e: 46 | print(f"'{repo}:{tag}' not found for project '{project}'") 47 | except StatusError as e: 48 | # catch all other HTTP exceptions 49 | ``` 50 | 51 | ## Inspecting errors 52 | 53 | The [`StatusError.errors`][harborapi.exceptions.StatusError.errors] attribute is a list of [`Error`][harborapi.models.models.Error] objects that contains more detailed information about the error(s) that have occured. 54 | 55 | ```py 56 | try: 57 | await client.delete_artifact("project", "repository", "latest") 58 | except StatusError as e: 59 | for error in e.errors: 60 | print(error.code, error.message) 61 | ``` 62 | 63 | An [`Error`][harborapi.models.models.Error] object has the following structure: 64 | 65 | ```py 66 | class Error(BaseModel): 67 | code: Optional[str] = Field(None, description="The error code") 68 | message: Optional[str] = Field(None, description="The error message") 69 | ``` 70 | 71 | (It is likely that `code` and `message` are both not `None` on runtime, but the API specification states that both these fields are optional.) 72 | -------------------------------------------------------------------------------- /docs/usage/ext/_example_callback.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import os 5 | from typing import List 6 | 7 | from httpx._exceptions import HTTPError 8 | 9 | from harborapi import HarborAsyncClient 10 | from harborapi.ext import api 11 | 12 | client = HarborAsyncClient( 13 | url=os.getenv("HARBOR_URL"), 14 | basicauth=os.getenv("HARBOR_CREDENTIALS"), 15 | ) 16 | 17 | 18 | def handle_exceptions(exceptions: List[Exception]) -> None: 19 | if not exceptions: 20 | return 21 | print("The following exceptions occurred:") 22 | for e in exceptions: 23 | if isinstance(e, HTTPError): 24 | print(f"HTTPError: {e.request.method} {e.request.url}") 25 | else: 26 | print(e) 27 | 28 | 29 | async def main() -> None: 30 | artifacts = await api.get_artifacts( 31 | client, 32 | projects=["library"], 33 | callback=handle_exceptions, 34 | ) 35 | for artifact in artifacts: 36 | print(artifact.artifact.digest) 37 | 38 | 39 | if __name__ == "__main__": 40 | asyncio.run(main()) 41 | -------------------------------------------------------------------------------- /docs/usage/ext/_example_get_artifact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import os 5 | 6 | from harborapi import HarborAsyncClient 7 | from harborapi.ext import api 8 | 9 | client = HarborAsyncClient( 10 | url=os.getenv("HARBOR_URL"), 11 | basicauth=os.getenv("HARBOR_CREDENTIALS"), 12 | ) 13 | 14 | 15 | async def main() -> None: 16 | artifacts = await api.get_artifacts( 17 | client, 18 | projects=["library", "mirrors"], 19 | repositories=["alpine", "busybox", "debian", "internal-repo"], 20 | tag="latest", 21 | ) 22 | for artifact in artifacts: 23 | print(artifact.artifact.digest) 24 | 25 | 26 | if __name__ == "__main__": 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /docs/usage/ext/api.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | The `ext.api` module contains helper functions that take in a `HarborAsyncClient` and use it to provide new or extended functionality. In most cases, the functions return [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] objects, which is composed of an artifact, its repository and optionally also the artifact's complete vulnerability report. 4 | 5 | The module makes heavy use of concurrent requests to speed up the process of fetching information from the Harbor API. This is done using the [`asyncio`][asyncio] module, which is part of the Python standard library. 6 | 7 | ## Handling exceptions in bulk operations 8 | 9 | Certain functions, such as [`get_artifacts`][harborapi.ext.api.get_artifacts] and [`get_artifact_vulnerabilities`][harborapi.ext.api.get_artifact_vulnerabilities] send a large number of requests. In order to not fail the entire operation if a single request fails, exceptions are ignored by default. 10 | 11 | To handle these exceptions, a `callback` parameter is available for these functions, which takes a function that receives a list of exceptions as its only argument. This callback function can be used to handle exceptions that occur during the concurrent requests. The function always fires even if there are no exceptions. If no callback function is specified, exceptions are ignored. 12 | 13 | ### Example 14 | 15 | ```py hl_lines="15-23 30" 16 | import asyncio 17 | import os 18 | from typing import List 19 | 20 | from harborapi import HarborAsyncClient 21 | from harborapi.ext import api 22 | from httpx._exceptions import HTTPError 23 | 24 | client = HarborAsyncClient( 25 | url=os.getenv("HARBOR_URL"), 26 | basicauth=os.getenv("HARBOR_CREDENTIALS"), 27 | ) 28 | 29 | 30 | def handle_exceptions(exceptions: List[Exception]) -> None: 31 | if not exceptions: 32 | return 33 | print("The following exceptions occurred:") 34 | for e in exceptions: 35 | if isinstance(e, HTTPError): 36 | print(f"HTTPError: {e.request.method} {e.request.url}") 37 | else: 38 | print(e) 39 | 40 | 41 | async def main() -> None: 42 | artifacts = await api.get_artifacts( 43 | client, 44 | projects=["library"], 45 | callback=handle_exceptions, 46 | ) 47 | for artifact in artifacts: 48 | print(artifact.artifact.digest) 49 | 50 | 51 | if __name__ == "__main__": 52 | asyncio.run(main()) 53 | ``` 54 | 55 | ## Fetch multiple artifacts concurrently 56 | 57 | 58 | The previous error handling example showcased the [`harborapi.ext.api.get_artifacts`][harborapi.ext.api.get_artifacts] function in some capacity. By default this function fetches all artifacts in all repositories in all projects and returns a list of [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] objects. 59 | 60 | Using the `project`, `repository` and `tag` parameters, we can limit the artifacts that are fetched. 61 | 62 | * The `project` parameter can be a list of one or more project names. 63 | * The `repository` parameter can be a list of one or more repositories (without the project name, e.g. `foo` instead of `library/foo`). 64 | * The `tag` parameter is a single tag name to filter on. 65 | 66 | ### Example 67 | 68 | ```py hl_lines="15-17" 69 | import asyncio 70 | import os 71 | 72 | from harborapi import HarborAsyncClient 73 | from harborapi.ext import api 74 | 75 | client = HarborAsyncClient( 76 | url=os.getenv("HARBOR_URL"), 77 | basicauth=os.getenv("HARBOR_CREDENTIALS"), 78 | ) 79 | 80 | async def main() -> None: 81 | artifacts = await api.get_artifacts( 82 | client, 83 | projects=["library", "mirrors"], 84 | repositories=["alpine", "busybox", "debian", "internal-repo"], 85 | tag="latest", 86 | ) 87 | for artifact in artifacts: 88 | print(artifact.artifact.digest) 89 | 90 | 91 | if __name__ == "__main__": 92 | asyncio.run(main()) 93 | ``` 94 | 95 | The `query` parameter, found on most [`HarborAsyncClient`][harborapi.client.HarborAsyncClient] methods, can also be passed to [`harborapi.ext.api.get_artifacts`][harborapi.ext.api.get_artifacts] for a more granular filtering of artifacts. 96 | 97 | See [HarborAsyncClient.get_artifacts][harborapi.client.HarborAsyncClient.get_artifacts] for more information on the `query` parameter. 98 | -------------------------------------------------------------------------------- /docs/usage/ext/artifact.md: -------------------------------------------------------------------------------- 1 | # Artifact info 2 | 3 | The `harborapi.ext.artifact` module defines the [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] class, which is a class that is composed of multiple Harbor API models: 4 | 5 | * [`Artifact`][harborapi.models.models.Artifact] 6 | * [`Repository`][harborapi.models.models.Repository] 7 | * [`HarborVulnerabilityReport`][harborapi.models.scanner.HarborVulnerabilityReport] 8 | 9 | Which in simplified Python code looks like this: 10 | 11 | ```py 12 | class ArtifactInfo: 13 | artifact: Artifact 14 | repository: Repository 15 | report: HarborVulnerabilityReport 16 | ``` 17 | 18 | The [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] class thus provides the complete information for a given artifact, including its repository and its vulnerability report. This makes all the information about an artifact available in one place. 19 | 20 | Several helper methods are defined to make use of the information available in the `ArtifactInfo` object. See the [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] object reference for more information. 21 | 22 | Most functions defined in `harborapi.ext.api` return [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] objects (or lists of them), unless otherwise specified. 23 | 24 | 25 | ## Why `ArtifactInfo`? 26 | 27 | The [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] class exists because the full information about an artifact is not returned by [`HarborAsyncClient.get_artifact`][harborapi.client.HarborAsyncClient.get_artifact] due to the way the API specification is written. The API specification for an [`Artifact`][harborapi.models.models.Artifact] does not include its repository name (the name by which you usally refer to the artifact, e.g. _library/hello-world_), nor its vulnerabilities. 28 | 29 | To that end, we also need to fetch the artifact's [`Repository`][harborapi.models.models.Repository] in a separate API call. This gives us the project name and the repository name for the artifact, among other things. 30 | 31 | Furthermore, if we wish to fetch the vulnerabilities of an Artifact, we need to fetch its [`HarborVulnerabilityReport`][harborapi.models.scanner.HarborVulnerabilityReport]. This is, again, a separate API call. The report we get from [`HarborAsyncClient.get_artifact(..., with_scan_overview=True)`][harborapi.client.HarborAsyncClient.get_artifact] is not sufficient, as it is merely an overview of the vulnerabilities, not the full report. Hence the need for this separate API call. 32 | 33 | Together, these 3 models combine to make an [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] object. 34 | 35 | Through functions such as `harborapi.ext.get_artifacts` and `harborapi.ext.get_artifact_vulnerabilities`, we can fetch multiple artifacts and their associated repo and report with a single function call, which also executes the requests concurrently. This is much more efficient than fetching each artifact, repo, and report individually and in sequence. 36 | -------------------------------------------------------------------------------- /docs/usage/ext/index.md: -------------------------------------------------------------------------------- 1 | # `harborapi.ext`: Extended functionality 2 | 3 | !!! note 4 | This module is for advanced users only. It's highly recommended to become familiar with the regular endpoint methods first, then come back to `harborapi.ext` if you need more advanced functionality such as concurrent requests, bulk operations, and aggregation of artifact vulnerabilities. 5 | 6 | For the vast majority of use cases, the regular endpoint methods are sufficient. 7 | 8 | The `harborapi.ext` module contains extensions and utility functions that are not part of the Harbor API spec. It expands the functionality of `harborapi` by providing additional 9 | functionality for common task such as fetching artifacts in bulk from multiple repositories. These functions are primarily found in [`harborapi.ext.api`](../../reference/ext/api.md). 10 | 11 | `harborapi.ext` also provides models used to combine multiple Harbor API models and aggregate their data. See: 12 | 13 | * [`harborapi.ext.artifact.ArtifactInfo`](./artifact.md) 14 | * [`harborapi.ext.report.ArtifactReport`](./report.md) 15 | 16 | 17 | To get a practical understanding of the module, check out some recipes that use it: 18 | 19 | * [Fetch artifacts concurrently](../../recipes/ext/conc-artifact.md) 20 | * [Get artifact owner](../../recipes/ext/artifactowner.md) 21 | * [Get vulnerabilities for all artifacts](../../recipes/ext/artifact-vulns.md) 22 | * [Fetch repositories concurrently](../../recipes/ext/conc-repo.md) 23 | 24 | 25 | !!!warning The `harborapi.ext` module is not part of the Harbor API specification and is not guaranteed to be stable. It may change in future versions of `harborapi`. 26 | 27 | Importing `harborapi.ext` is optional and does not require any additional dependencies. 28 | 29 | ```py 30 | import harborapi.ext 31 | # or 32 | from harborapi import ext 33 | # or 34 | from harborapi.ext import api, artifact, cve, report 35 | # or 36 | from harborapi.ext.api import get_artifact_info, get_artifact_vulnerabilities #, ... 37 | ``` 38 | 39 | Your IDE should provide auto-completion for the various imports available from the `harborapi.ext` module. Otherwise, check the [Reference](../../reference/index.md) 40 | -------------------------------------------------------------------------------- /docs/usage/ext/report.md: -------------------------------------------------------------------------------- 1 | # Report 2 | 3 | The `ext.report` module defines the [`ArtifactReport`][harborapi.ext.report.ArtifactReport] class, which aggregates several [`ArtifactInfo`][harborapi.ext.artifact.ArtifactInfo] objects. Through this class, one can query the aggregated data for all artifacts affected by a given vulnerability, all artifacts who have a given vulnerable package, etc. 4 | 5 | This allows for a deeper analysis of the vulnerabilities affecting your artifacts, and can be used to generate reports, or to take action on the artifacts that are affected by a given vulnerability. 6 | 7 | Given a list of ArtifactInfo objects, we can query the aggregated data to find all artifacts affected by a given vulnerability: 8 | 9 | ```py hl_lines="11" 10 | from harborapi import HarborAsyncClient 11 | from harborapi.ext.api import get_artifact_vulnerabilities 12 | from harborapi.ext.report import ArtifactReport 13 | 14 | client = HarborAsyncClient(...) 15 | 16 | artifacts = await get_artifact_vulnerabilities(client) 17 | 18 | # Instantiate the ArtifactReport from the fetched artifacts 19 | report = ArtifactReport(artifacts) 20 | filtered_report = report.with_cve("CVE-2020-0001") 21 | 22 | # iterating on ArtifactReport yields ArtifactInfo objects 23 | for artifact in filtered_report: 24 | print(artifact.repository.name, artifact.artifact.digest) 25 | ``` 26 | 27 | All `ArtifactReport.with_*` methods return new ArtifactReport objects. 28 | 29 | ## More granular package filtering 30 | 31 | We can also query the report for all artifacts who have a given vulnerable package: 32 | 33 | ```py 34 | filtered_report = report.with_package("openssl") 35 | ``` 36 | 37 | The search is case-insensitive by default, but can be made case-sensitive by setting the `case_sensitive` argument to `True`: 38 | 39 | ```py hl_lines="3" 40 | filtered_report = report.with_package( 41 | "OpenSSL", # WARNING: package is likely named openssl! 42 | case_sensitive=True, 43 | ) 44 | ``` 45 | 46 | We can further narrow down the results by specifying minimum and/or maximum versions of the package: 47 | 48 | ```py hl_lines="3 4" 49 | filtered_report = report.with_package( 50 | "openssl", 51 | min_version=(3, 0, 0), 52 | max_version=(3, 0, 2) 53 | ) 54 | ``` 55 | 56 | All text-based queries support regular expressions. For example, to find all artifacts with a package name that starts with `openssl`: 57 | 58 | ```py 59 | filtered_report = report.with_package("openssl.*") 60 | ``` 61 | 62 | ## Chaining filters 63 | 64 | As previously mentioned, all `ArtifactReport.with_*` methods return new [`ArtifactReport`][harborapi.ext.report.ArtifactReport] objects, so they can be chained together to easily filter a report with multiple criteria. 65 | 66 | ```py 67 | filtered_report = ( 68 | report.with_package("openssl") 69 | .with_cve("CVE-2020-0001") 70 | .with_repository("my-repo") 71 | ) .with_tag("latest") 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/usage/index.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | The `harborapi` library provides a class called [`HarborAsyncClient`][harborapi.HarborAsyncClient] that can be used to interact with the Harbor API. To use it, you need to create a `HarborAsyncClient` instance with your Harbor instance's API URL, as well as some authentication credentials. 4 | 5 | The endpoint methods in the `HarborAsyncClient` class are all asynchronous, which means they can only be called inside an async function using the `await` keyword. Here's an example of using the [`get_project()`][harborapi.HarborAsyncClient.get_project] method: 6 | 7 | ```python 8 | import asyncio 9 | from harborapi import HarborAsyncClient 10 | 11 | client = HarborAsyncClient( 12 | url="https://harbor.example.com/api/v2.0", 13 | username="admin", 14 | secret="password", 15 | ) 16 | 17 | async def main() -> None: 18 | project = await client.get_project("library") 19 | print(project) 20 | 21 | asyncio.run(main()) 22 | ``` 23 | 24 | For a full list of implemented endpoints on `HarborAsyncClient`, check out the [Endpoints](../endpoints/index.md) page. If you're new to asyncio, you can find a good introduction in the [FastAPI package's docs](https://fastapi.tiangolo.com/async/#async-and-await). You can also find more examples in the [Recipes](../recipes/index.md) page. Lastly, the [offical Python asyncio documentation](https://docs.python.org/3/library/asyncio.html) contains the complete reference for the `asyncio` module as well as examples of how it's used. 25 | 26 | There are several ways to authenticate with the Harbor API, and they are documented on the [Authentication](authentication.md) page. The [Methods](./methods/index.md) page shows basic usage of the different types of methods exposed by the client object. 27 | -------------------------------------------------------------------------------- /docs/usage/logging.md: -------------------------------------------------------------------------------- 1 | # Logging 2 | 3 | The library uses the standard Python logging library for logging purposes, and provides a single logger named `harborapi`. The logger is disabled by default, but can be enabled if desired. 4 | 5 | 6 | ## Enable logging 7 | 8 | Logging can be enabled by passing in `logging=True` to the client constructor: 9 | 10 | ```python hl_lines="7" 11 | from harborapi import HarborAsyncClient 12 | 13 | client = HarborAsyncClient( 14 | url="https://harbor.example.com/api/v2.0", 15 | username="admin", 16 | secret="password", 17 | logging=True, 18 | ) 19 | ``` 20 | 21 | Alternatively, it can be enabled by setting the `HARBORAPI_LOGGING` environment variable to `1`: 22 | 23 | ```bash 24 | HARBORAPI_LOGGING=1 python myscript.py 25 | ``` 26 | 27 | When logging is enabled, the handler will be configured to log to stderr with the log level set to `INFO`. 28 | 29 | ## Configure logging 30 | 31 | Should you wish to configure the logger, you can import it and configure it as you would any other logger: 32 | 33 | ```python 34 | import logging 35 | from harborapi.log import logger 36 | 37 | logger.setLevel(logging.DEBUG) 38 | # ... other changes to the logger 39 | ``` 40 | 41 | ## Limitations 42 | 43 | * Currently, the library uses a single logger for all logging purposes. This means that it is not possible to enable logging for only a specific part of the library or individually configure loggers for multiple clients. 44 | * Configuring logging for one `HarborAsyncClient` instance will affect all other instances, should you have multiple client instances. 45 | * The library does not support changing the log level through the `HarborAsyncClient` constructor nor env vars. If you wish to change the log level, you must do so through the logger itself. See [Configure logging](#configure-logging) for an example of how to do this. 46 | * Logging to streams other than stderr is not directly supported through the `HarborAsyncClient` constructor. However, you can configure the logger object to change the handler or add one to log to a different stream. 47 | -------------------------------------------------------------------------------- /docs/usage/methods/delete.md: -------------------------------------------------------------------------------- 1 | # Delete 2 | 3 | Endpoints that delete resources usually require a resource identifier or name as the first argument. Most of these endpoints return `None` on success. Failure to delete a resource will raise an exception. 4 | 5 | ```py 6 | import asyncio 7 | from harborapi import HarborAsyncClient 8 | 9 | client = HarborAsyncClient(...) 10 | 11 | 12 | async def main() -> None: 13 | await client.delete_project("test-project") 14 | 15 | 16 | asyncio.run(main()) 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/usage/methods/index.md: -------------------------------------------------------------------------------- 1 | # Methods 2 | 3 | The following sections demonstrate basic usage of the three main categories of methods exposed by the client object. 4 | 5 | * [Reading](read.md) 6 | * [Creating/updating](create-update.md) 7 | * [Deleting](delete.md) 8 | 9 | 10 | For a complete list of methods, see the [Endpoints Overview](../../endpoints/index.md). 11 | 12 | See [Recipes](../../recipes/index.md) for concrete examples of how to use the methods to perform common tasks. 13 | -------------------------------------------------------------------------------- /docs/usage/responselog.md: -------------------------------------------------------------------------------- 1 | # Response log 2 | 3 | The `HarborAsyncClient` keeps track of all responses it receives in a response log. This is a [`ResponseLog`][harborapi.responselog.ResponseLog] object, which contains a list of [`ResponseLogEntry`][harborapi.responselog.ResponseLogEntry] objects, and can be accessed via the [`response_log`][harborapi.client.HarborAsyncClient.response_log] attribute of the client. Each entry contains the request URL, the request method, the response status code, the response duration, and the size of the response body. 4 | 5 | 6 | 7 | ```py 8 | from harborapi import HarborAsyncClient 9 | 10 | client = HarborAsyncClient(...) 11 | 12 | await client.get_system_info() 13 | await client.get_system_info() 14 | await client.get_system_info() 15 | 16 | print(client.response_log) 17 | ``` 18 | 19 | ``` 20 | [ 21 | , 22 | , 23 | , 24 | ] 25 | ``` 26 | 27 | [`ResponseLog`][harborapi.responselog.ResponseLog] behaves like a sized iterable and supports indexing, iteration and sizing: 28 | 29 | ```py 30 | client.response_log[0] 31 | client.response_log[-1] 32 | client.response_log[1:3] 33 | len(client.response_log) 34 | for response in client.response_log: 35 | pass 36 | ``` 37 | 38 | ## Last response 39 | 40 | The last response can be accessed via the [`last_response`][harborapi.client.HarborAsyncClient.last_response] attribute of the client: 41 | 42 | ```py 43 | print(client.last_response) 44 | ``` 45 | 46 | ```py 47 | 48 | ``` 49 | 50 | If no responses are stored, this returns `None`. 51 | 52 | ## Limiting log size 53 | 54 | By specifying the `max_logs` parameter when constructing the client, the response log will be limited to the specified number of responses. 55 | 56 | 57 | ```py 58 | from harborapi import HarborAsyncClient 59 | 60 | client = HarborAsyncClient(..., max_logs=2) 61 | 62 | await client.get_system_info() 63 | await client.get_system_info() 64 | await client.get_system_info() 65 | 66 | print(client.response_log) 67 | ``` 68 | 69 | ```py 70 | [ 71 | , 72 | , 73 | ] 74 | ``` 75 | 76 | The response log operates with a FIFO (first in, first out) policy, meaning that the oldest response will be removed when the log is full. 77 | 78 | ### Adjusting the limit 79 | 80 | The maximum size of the response log can be adjusted on the fly with the [`ResponseLog.resize()`][harborapi.responselog.ResponseLog.resize] method: 81 | 82 | ```py 83 | assert len(client.response_log) > 3 84 | client.response_log.resize(3) 85 | assert len(client.response_log) == 3 86 | ``` 87 | 88 | `ResponseLog.resize()` accepts a single integer argument which specifies the new maximum size of the log. If the new size is smaller than the current size, the log discards the oldest responses until the new size is reached. 89 | 90 | ## Clear the log 91 | 92 | The response log can be cleared with the [`ResponseLog.clear()`][harborapi.responselog.ResponseLog.clear] method: 93 | 94 | ```py 95 | client.response_log.clear() 96 | assert len(client.response_log) == 0 97 | ``` 98 | -------------------------------------------------------------------------------- /harborapi/__about__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | __version__ = "0.26.2" 4 | -------------------------------------------------------------------------------- /harborapi/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import _types 4 | from . import auth 5 | from . import client 6 | from . import client_sync 7 | from . import exceptions 8 | from . import ext 9 | from . import models 10 | from . import utils 11 | from . import version 12 | from .__about__ import __version__ as __version__ 13 | from .client import HarborAsyncClient 14 | from .client_sync import HarborClient 15 | 16 | # Import after everything else to avoid circular imports 17 | 18 | 19 | __all__ = [ 20 | "HarborAsyncClient", 21 | "HarborClient", 22 | "auth", 23 | "models", 24 | "client", 25 | "client_sync", 26 | "exceptions", 27 | "ext", 28 | "_types", 29 | "utils", 30 | "version", 31 | ] 32 | -------------------------------------------------------------------------------- /harborapi/_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | from typing import Dict 5 | from typing import List 6 | from typing import MutableMapping 7 | from typing import Sequence 8 | from typing import Union 9 | 10 | from httpx._types import PrimitiveData 11 | 12 | JSONType = Union[Dict[str, Any], List[Any]] # TODO: Use PrimitiveData 13 | 14 | 15 | # HTTP(X) 16 | QueryParamValue = Union[PrimitiveData, Sequence[PrimitiveData]] 17 | QueryParamMapping = MutableMapping[str, QueryParamValue] 18 | -------------------------------------------------------------------------------- /harborapi/client_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import inspect 5 | from typing import Any 6 | from typing import Callable 7 | from typing import Optional 8 | 9 | from .client import HarborAsyncClient 10 | 11 | 12 | class HarborClient(HarborAsyncClient): 13 | """Non-async Harbor API client.""" 14 | 15 | def __init__( 16 | self, 17 | loop: Optional[asyncio.AbstractEventLoop] = None, 18 | *args: Any, 19 | **kwargs: Any, 20 | ): 21 | super().__init__(*args, **kwargs) 22 | self.loop = loop or asyncio.new_event_loop() 23 | asyncio.set_event_loop(self.loop) 24 | 25 | def __getattribute__(self, name: str) -> Any: 26 | """Overrides the `__getattribute__` method to wrap coroutine functions 27 | 28 | Intercepts attribute access and wraps coroutine functions with `_wrap_coro`. 29 | 30 | Internal methods are not wrapped in order to run them normally in 31 | an asynchronous manner within the event loop. 32 | """ 33 | attr = super().__getattribute__(name) 34 | name = name.lower() 35 | 36 | # Filter out internal methods 37 | if name.startswith("_") or any( 38 | name == http_method 39 | for http_method in ( 40 | "get", 41 | "get_text", # get for text/plain (hack) 42 | "post", 43 | "put", 44 | "patch", 45 | "delete", 46 | "head", 47 | "options", 48 | ) 49 | ): 50 | return attr 51 | 52 | if inspect.iscoroutinefunction(attr): 53 | return self._wrap_coro(attr) 54 | 55 | return attr 56 | 57 | def _wrap_coro(self, coro: Any) -> Callable[[Any], Any]: 58 | """Wraps a coroutine function in an `AbstractEventLoop.run_until_complete()` 59 | call that runs the coroutine in the event loop. 60 | 61 | This is a hacky way to make the client behave like a synchronous client. 62 | 63 | Parameters 64 | ---------- 65 | coro : Any 66 | The coroutine function to wrap. 67 | 68 | Returns 69 | ------- 70 | Callable[[Any], Any] 71 | A function that runs the coroutine in the event loop. 72 | """ 73 | 74 | def wrapper(*args: Any, **kwargs: Any) -> Any: # TODO: better type signature 75 | return self.loop.run_until_complete(coro(*args, **kwargs)) 76 | 77 | return wrapper 78 | -------------------------------------------------------------------------------- /harborapi/ext/__init__.py: -------------------------------------------------------------------------------- 1 | """The ext module contains extensions and utility functions that are not 2 | part of the Harbor API. 3 | 4 | It expands the functionality of the Harbor API by providing additional 5 | functionality for common task such as fetching all artifacts and their 6 | vulnerabilities in one or more repository or project. 7 | 8 | Furthermore, it contains models for combining multiple Harbor API models 9 | and aggregating their data. 10 | 11 | 12 | ---------------- 13 | 14 | Notes on `ArtifactInfo` vs `ArtifactReport` 15 | 16 | 17 | The `ArtifactInfo` and ArtifactReport models are similar in that they both 18 | provide similar interfaces, but the data they operate on is different: 19 | 20 | `ArtifactInfo` operates on a single artifact and its associated repository and vulnerability report. 21 | It provides methods for filtering and querying vulnerabilities using a broad range 22 | of criteria, such as severity, package name, and CVE ID. 23 | 24 | `ArtifactReport` operates on a list of ArtifactInfo objects. It provides methods 25 | for aggregating information from multiple artifacts and their vulnerabilities. 26 | 27 | `with_*` methods on `ArtifactReport` return a new `ArtifactReport` object with 28 | only the `ArtifactInfo` objects that match the given criteria, while `with_*` methods on 29 | `ArtifactInfo` return a list of _vulnerabilities_ that match the criteria. 30 | This difference can be summarized as the following: 31 | 32 | ArtifactInfo.with_* -> List[Vulnerability] 33 | ArtifactReport.with_* -> ArtifactReport 34 | """ 35 | 36 | from __future__ import annotations 37 | 38 | from .api import * 39 | from .artifact import ArtifactInfo 40 | from .cve import * 41 | from .report import ArtifactReport 42 | from .report import Vulnerability 43 | -------------------------------------------------------------------------------- /harborapi/ext/cve.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from itertools import chain 4 | from typing import TYPE_CHECKING 5 | 6 | from pydantic import BaseModel 7 | 8 | from . import stats 9 | 10 | if TYPE_CHECKING: 11 | from .artifact import ArtifactInfo 12 | from .report import ArtifactReport 13 | 14 | 15 | class CVSSData(BaseModel): 16 | """Key CVSS statistics for a scanned artifact.""" 17 | 18 | mean: float 19 | median: float 20 | stdev: float 21 | min: float 22 | max: float 23 | 24 | @classmethod 25 | def from_artifactinfo(cls, artifact: "ArtifactInfo") -> "CVSSData": 26 | """Create a CVSSData instance from an ArtifactInfo object. 27 | 28 | Parameters 29 | ---------- 30 | artifact : ArtifactInfo 31 | The artifact to extract CVSS data from. 32 | 33 | Returns 34 | ------- 35 | CVSSData 36 | The CVSS data for the artifact. 37 | 38 | See Also 39 | -------- 40 | [ArtifactInfo.cvss][harborapi.ext.artifact.ArtifactInfo.cvss] 41 | """ 42 | scores = artifact.report.cvss_scores 43 | return cls( 44 | mean=stats.mean(scores), 45 | median=stats.median(scores), 46 | stdev=stats.stdev(scores), 47 | min=stats.min(scores), 48 | max=stats.max(scores), 49 | ) 50 | 51 | @classmethod 52 | def from_report(cls, report: "ArtifactReport") -> "CVSSData": 53 | """Create a CVSSData instance from an ArtifactReport object. 54 | 55 | Parameters 56 | ---------- 57 | report : ArtifactReport 58 | The report to extract CVSS data from. 59 | 60 | Returns 61 | ------- 62 | CVSSData 63 | The CVSS data for the report. 64 | """ 65 | # Wrap generator in list to allow for re-use 66 | scores = list( 67 | chain.from_iterable([a.report.cvss_scores for a in report.artifacts]) 68 | ) 69 | return cls( 70 | mean=stats.mean(scores), 71 | median=stats.median(scores), 72 | stdev=stats.stdev(scores), 73 | min=stats.min(scores), 74 | max=stats.max(scores), 75 | ) 76 | -------------------------------------------------------------------------------- /harborapi/ext/regex.py: -------------------------------------------------------------------------------- 1 | """Caching functions for the ext module.""" 2 | 3 | from __future__ import annotations 4 | 5 | import re 6 | from functools import lru_cache 7 | from typing import Dict 8 | from typing import Optional 9 | from typing import Tuple 10 | 11 | from ..log import logger 12 | 13 | # NOTE: Regex type generics require >=3.9. Have to wrap in a string. 14 | 15 | # Unbounded cache (should be fine since we're only caching regex patterns) 16 | _pattern_cache: Dict[Tuple[str, bool], "re.Pattern[str]"] = {} 17 | 18 | # TODO: bake get_pattern() into match(), so we only have to call match() in the codebase. 19 | 20 | 21 | # NOTE: could we just do away with _pattern_cache and add lru_cache to the 22 | # get_pattern function? I think we could, but we should test the performance of both approaches. 23 | def get_pattern(pattern: str, case_sensitive: bool = False) -> "re.Pattern[str]": 24 | """Simple cache function for getting/setting compiled regex patterns. 25 | 26 | Parameters 27 | ---------- 28 | pattern : str 29 | The regex pattern to compile. 30 | case_sensitive : bool, optional 31 | Whether the pattern should be case sensitive, by default False 32 | 33 | Returns 34 | ------- 35 | re.Pattern[str] 36 | The compiled regex pattern. 37 | """ 38 | cache_key = (pattern, case_sensitive) 39 | if cache_key not in _pattern_cache: 40 | flags = re.IGNORECASE if not case_sensitive else 0 41 | _pattern_cache[cache_key] = re.compile(pattern, flags=flags) 42 | return _pattern_cache[cache_key] 43 | 44 | 45 | @lru_cache(maxsize=128) 46 | def match(pattern: "re.Pattern[str]", s: str) -> Optional["re.Match[str]"]: 47 | try: 48 | return pattern.match(s) 49 | except Exception as e: 50 | logger.error("Error matching pattern %s to string %s: %s", pattern, s, e) 51 | return None 52 | -------------------------------------------------------------------------------- /harborapi/ext/stats.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import statistics 4 | from typing import Any 5 | from typing import Callable 6 | from typing import Iterable 7 | 8 | from ..log import logger 9 | 10 | __all__ = [ 11 | "mean", 12 | "median", 13 | "stdev", 14 | "min", 15 | "max", 16 | ] 17 | 18 | _min = min 19 | _max = max 20 | DEFAULT_VALUE = 0.0 21 | 22 | 23 | def mean(a: Iterable[float]) -> float: 24 | return _do_stats_math(statistics.mean, a) 25 | 26 | 27 | def median(a: Iterable[float]) -> float: 28 | return _do_stats_math(statistics.median, a) 29 | 30 | 31 | def stdev(a: Iterable[float]) -> float: 32 | return _do_stats_math(statistics.stdev, a) 33 | 34 | 35 | def min(a: Iterable[float]) -> float: 36 | return _min(a, default=DEFAULT_VALUE) 37 | 38 | 39 | def max(a: Iterable[float]) -> float: 40 | return _max(a, default=DEFAULT_VALUE) 41 | 42 | 43 | def _do_stats_math( 44 | func: Callable[[Any], float], 45 | a: Iterable[float], 46 | default: float = DEFAULT_VALUE, 47 | filter_none: bool = False, 48 | ) -> float: 49 | """Wrapper function around stats functions that handles exceptions.""" 50 | if filter_none: 51 | a = filter(None, a) 52 | 53 | # Try to run the statistics function, but if it fails, return the default value 54 | # Functions like stdev, median and mean will fail if there is only one data point 55 | # or no data points. In these cases, we want to return the default value. 56 | try: 57 | res = func(a) 58 | except statistics.StatisticsError: 59 | logger.error("%s(%s) failed. Defaulting to %s", func.__name__, repr(a), default) 60 | return float(default) 61 | return float(res) 62 | -------------------------------------------------------------------------------- /harborapi/log.py: -------------------------------------------------------------------------------- 1 | # I am somewhat clueless about best practices for configuring logging for a library. 2 | # The contents of this module are derived from a conversation with ChatGPT (GPT-4). 3 | # NOTE: multiple clients with different `logging` argument values will 4 | # cause problems. However, users should not be creating multiple clients, so 5 | # this should not be an issue. If it is, we can add a warning. 6 | from __future__ import annotations 7 | 8 | import logging 9 | 10 | DEFAULT_LEVEL = logging.INFO 11 | DEFAULT_LEVEL_DISABLED = logging.CRITICAL 12 | 13 | logger = logging.getLogger("harborapi") 14 | 15 | 16 | def enable_logging(level: int = DEFAULT_LEVEL) -> None: 17 | """Enable logging for 'harborapi'. 18 | 19 | Parameters 20 | ---------- 21 | level : int 22 | The logging level, by default logging.INFO 23 | """ 24 | logger.setLevel(level) 25 | handler = logging.StreamHandler() # Logs to stderr by default 26 | formatter = logging.Formatter( 27 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 28 | ) 29 | handler.setFormatter(formatter) 30 | if logger.hasHandlers(): 31 | logger.handlers.clear() # Remove existing handlers 32 | logger.addHandler(handler) 33 | 34 | 35 | def disable_logging() -> None: 36 | """Disable logging for 'harborapi'""" 37 | logger.setLevel(DEFAULT_LEVEL_DISABLED) 38 | if logger.hasHandlers(): 39 | logger.handlers.clear() # Remove existing handlers 40 | # Add a null handler to prevent "No handler found" warnings 41 | logger.addHandler(logging.NullHandler()) 42 | 43 | 44 | # Disable logging by default 45 | disable_logging() 46 | -------------------------------------------------------------------------------- /harborapi/models/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import scanner 4 | from .buildhistory import * 5 | from .models import * 6 | from .oidc import * 7 | from .scanner import Artifact as ScanArtifact 8 | from .scanner import Error as ScanError 9 | from .scanner import HarborVulnerabilityReport 10 | from .scanner import Registry as ScanRegistry 11 | from .scanner import Scanner as ScanScanner 12 | from .scanner import ScannerAdapterMetadata as ScanScannerAdapterMetadata 13 | from .scanner import ScannerCapability as ScanScannerCapability 14 | from .scanner import Severity 15 | from .scanner import VulnerabilityItem as VulnerabilityItem 16 | 17 | # Due to some overlap in the names of the models generated from the two schemas, 18 | # we need to explicitly import the conflicting models from the other schema prefixed 19 | # with 'Scan'. 20 | # 21 | # These models are different despite the overlapping names, so we can't just 22 | # import them both, as one will shadow the other. 23 | -------------------------------------------------------------------------------- /harborapi/models/_models.py: -------------------------------------------------------------------------------- 1 | """DEPRECATED: This module will be removed in a future version. 2 | Module kept only for backwards compatibility with old code generation scheme.""" 3 | 4 | from __future__ import annotations 5 | 6 | import warnings 7 | 8 | warnings.warn( 9 | "The harborapi.models._models module is deprecated and will be removed in a future version. Use harborapi.models.models instead.", 10 | DeprecationWarning, 11 | ) 12 | 13 | from .models import * 14 | -------------------------------------------------------------------------------- /harborapi/models/_scanner.py: -------------------------------------------------------------------------------- 1 | """DEPRECATED: This module will be removed in a future version. 2 | Module kept only for backwards compatibility with old code generation scheme.""" 3 | 4 | from __future__ import annotations 5 | 6 | import warnings 7 | 8 | warnings.warn( 9 | "The harborapi.models._scanner module is deprecated and will be removed in a future version. Use harborapi.models.scanner instead.", 10 | DeprecationWarning, 11 | ) 12 | 13 | from .scanner import * 14 | -------------------------------------------------------------------------------- /harborapi/models/buildhistory.py: -------------------------------------------------------------------------------- 1 | """Models defined here are part of the Harbor API, but not documented in the official schema. 2 | 3 | The models in this module are _NOT_ automatically generated. 4 | """ 5 | 6 | from __future__ import annotations 7 | 8 | from datetime import datetime 9 | from typing import Optional 10 | 11 | from .base import BaseModel 12 | 13 | 14 | # Unclear what is optional and what isn't 15 | class BuildHistoryEntry(BaseModel): 16 | created: datetime 17 | created_by: str 18 | author: Optional[str] = None 19 | empty_layer: bool = False 20 | -------------------------------------------------------------------------------- /harborapi/models/file.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Optional 4 | 5 | import httpx 6 | 7 | 8 | class FileResponse: 9 | """A response object for a file download.""" 10 | 11 | def __init__(self, response: httpx.Response) -> None: 12 | self.response = response 13 | 14 | @property 15 | def content(self) -> bytes: 16 | return self.response.content 17 | 18 | @property 19 | def encoding(self) -> Optional[str]: 20 | return self.response.encoding 21 | 22 | @property 23 | def content_type(self) -> Optional[str]: 24 | return self.response.headers.get("content-type", None) 25 | 26 | @property 27 | def headers(self) -> httpx.Headers: 28 | return self.response.headers 29 | 30 | def __bytes__(self) -> bytes: 31 | return self.content 32 | -------------------------------------------------------------------------------- /harborapi/models/mappings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from typing import Optional 5 | from typing import TypeVar 6 | 7 | if sys.version_info >= (3, 9): 8 | from collections import OrderedDict 9 | else: 10 | from typing import OrderedDict 11 | 12 | 13 | _KT = TypeVar("_KT") # key type 14 | _VT = TypeVar("_VT") # value type 15 | 16 | 17 | # NOTE: How to parametrize a normal dict in 3.8? In >=3.9 we can do `dict[_KT, _VT]` 18 | class FirstDict(OrderedDict[_KT, _VT]): 19 | """Dict with method to get its first value.""" 20 | 21 | def first(self) -> Optional[_VT]: 22 | """Return the first value in the dict or None if dict is empty.""" 23 | return next(iter(self.values()), None) 24 | -------------------------------------------------------------------------------- /harborapi/models/oidc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .base import BaseModel 4 | 5 | 6 | class OIDCTestReq(BaseModel): 7 | url: str 8 | verify_cert: bool 9 | -------------------------------------------------------------------------------- /harborapi/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/harborapi/py.typed -------------------------------------------------------------------------------- /harborapi/responselog.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections import deque 4 | from dataclasses import dataclass 5 | from typing import Deque 6 | from typing import Iterator 7 | from typing import NamedTuple 8 | from typing import Optional 9 | 10 | from httpx import URL 11 | 12 | 13 | class ResponseLogEntry(NamedTuple): 14 | """A log entry for an HTTP response.""" 15 | 16 | url: URL 17 | """An httpx.URL object representing the URL of the request. Can be cast to string using `str()`.""" 18 | method: str 19 | """The HTTP method used for the request.""" 20 | status_code: int 21 | """The HTTP status code of the response.""" 22 | duration: float 23 | """The duration of the full request/response cycle in seconds.""" 24 | response_size: int 25 | """The size of the response body in bytes.""" 26 | 27 | def __repr__(self) -> str: 28 | return f"" 29 | 30 | 31 | # NOTE: Could we do the same by subclassing deque? 32 | # We are re-implementing a lot of sequence methods here. 33 | @dataclass 34 | class ResponseLog: 35 | """A log of HTTP responses.""" 36 | 37 | entries: Deque[ResponseLogEntry] 38 | 39 | def __init__(self, max_logs: Optional[int] = None) -> None: 40 | """Initialize the log.""" 41 | self.entries = deque(maxlen=max_logs) 42 | 43 | def add(self, entry: ResponseLogEntry) -> None: 44 | """Add a new entry to the log.""" 45 | self.entries.append(entry) 46 | 47 | def resize(self, max_logs: int) -> None: 48 | """Resize the log to the specified maximum number of entries.""" 49 | self.entries = deque(self.entries, maxlen=max_logs) 50 | 51 | def clear(self) -> None: 52 | """Clear the log.""" 53 | self.entries.clear() 54 | 55 | def __iter__(self) -> Iterator[ResponseLogEntry]: 56 | """Return an iterator over the entries in the log.""" 57 | return iter(self.entries) 58 | 59 | def __getitem__(self, index: int) -> ResponseLogEntry: 60 | """Return the entry at the specified index.""" 61 | return self.entries[index] 62 | 63 | def __len__(self) -> int: 64 | """Return the number of entries in the log.""" 65 | return len(self.entries) 66 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import os 5 | from pathlib import Path 6 | from typing import Iterable 7 | from typing import Union 8 | 9 | import pytest 10 | from hypothesis import Verbosity 11 | from hypothesis import settings 12 | from pytest_httpserver import HTTPServer 13 | 14 | from harborapi.client import HarborAsyncClient 15 | from harborapi.log import enable_logging 16 | 17 | from .strategies import init_strategies 18 | 19 | # Enable logging for tests 20 | enable_logging() 21 | 22 | 23 | # Init custom hypothesis strategies 24 | init_strategies() 25 | 26 | # Hypothesis profiles 27 | settings.register_profile("ci", settings(max_examples=1000)) 28 | settings.register_profile( 29 | "debug", 30 | settings( 31 | max_examples=10, 32 | verbosity=Verbosity.verbose, 33 | ), 34 | ) 35 | settings.register_profile("dev", max_examples=10) 36 | settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default")) 37 | 38 | 39 | @pytest.fixture(scope="function") 40 | def httpserver(httpserver: HTTPServer) -> Iterable[HTTPServer]: 41 | yield httpserver 42 | if not httpserver.is_running(): 43 | httpserver.start() 44 | # Ensure server has no handlers after each test 45 | httpserver.clear_all_handlers() 46 | # Maybe run httpserver.clear() too? 47 | 48 | 49 | # must be set to "function" to make sure logging is enabled for each test 50 | @pytest.fixture(scope="function") 51 | def async_client(httpserver: HTTPServer) -> Iterable[HarborAsyncClient]: 52 | yield HarborAsyncClient( 53 | username="username", 54 | secret="secret", 55 | url=httpserver.url_for("/api/v2.0"), 56 | logging=True, 57 | ) 58 | 59 | 60 | @pytest.fixture(scope="function") 61 | def credentials_dict() -> dict: 62 | return { 63 | "creation_time": "2022-07-01T13:20:46.230Z", 64 | "description": "Some description", 65 | "disable": False, 66 | "duration": 30, 67 | "editable": True, 68 | "expires_at": 1659273646, 69 | "id": 1, 70 | "level": "system", 71 | "name": "robot$harborapi-test", 72 | "permissions": [ 73 | { 74 | "access": [ 75 | {"action": "list", "resource": "repository"}, 76 | {"action": "pull", "resource": "repository"}, 77 | ], 78 | "kind": "project", 79 | "namespace": "*", 80 | } 81 | ], 82 | "update_time": "2022-07-06T13:26:45.360Z", 83 | "permissionScope": { 84 | "coverAll": True, 85 | "access": [ 86 | {"action": "list", "resource": "repository"}, 87 | {"action": "pull", "resource": "repository"}, 88 | ], 89 | }, 90 | "secret": "bad-password", 91 | } 92 | 93 | 94 | @pytest.fixture(scope="function") 95 | def credentials_file(tmp_path: Path, credentials_dict: dict) -> Path: 96 | """Create a credentials file for testing""" 97 | credentials_file = tmp_path / "credentials.json" 98 | credentials_file.write_text(json.dumps(credentials_dict)) 99 | return credentials_file 100 | 101 | 102 | @pytest.fixture(params=["test", 1234]) 103 | def project_name_or_id(request: pytest.FixtureRequest) -> Union[str, int]: 104 | """Parametrized fixture that returns a project name (str) and/or id (int)""" 105 | return request.param 106 | -------------------------------------------------------------------------------- /tests/endpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/tests/endpoints/__init__.py -------------------------------------------------------------------------------- /tests/endpoints/test_audit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.models.models import AuditLog 14 | 15 | from ..utils import json_from_list 16 | 17 | 18 | @pytest.mark.asyncio 19 | @given(st.lists(st.builds(AuditLog))) 20 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 21 | async def test_get_audit_logs_mock( 22 | async_client: HarborAsyncClient, 23 | httpserver: HTTPServer, 24 | audit_logs: List[AuditLog], 25 | ): 26 | httpserver.expect_oneshot_request( 27 | "/api/v2.0/audit-logs", 28 | method="GET", 29 | ).respond_with_data( 30 | json_from_list(audit_logs), 31 | headers={"Content-Type": "application/json"}, 32 | ) 33 | 34 | logs = await async_client.get_audit_logs() 35 | assert logs == audit_logs 36 | -------------------------------------------------------------------------------- /tests/endpoints/test_configure.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models.models import Configurations 12 | from harborapi.models.models import ConfigurationsResponse 13 | 14 | 15 | @pytest.mark.asyncio 16 | @given(st.builds(ConfigurationsResponse)) 17 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 18 | async def test_get_config_mock( 19 | async_client: HarborAsyncClient, 20 | httpserver: HTTPServer, 21 | config: ConfigurationsResponse, 22 | ): 23 | httpserver.expect_oneshot_request( 24 | "/api/v2.0/configurations", 25 | method="GET", 26 | ).respond_with_json(config.model_dump(mode="json")) 27 | 28 | resp = await async_client.get_config() 29 | assert resp == config 30 | 31 | 32 | @pytest.mark.asyncio 33 | @given(st.builds(Configurations)) 34 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 35 | async def test_update_config_mock( 36 | async_client: HarborAsyncClient, 37 | httpserver: HTTPServer, 38 | config: Configurations, 39 | ): 40 | httpserver.expect_oneshot_request( 41 | "/api/v2.0/configurations", 42 | method="PUT", 43 | json=config.model_dump(mode="json", exclude_unset=True), 44 | ).respond_with_data() 45 | 46 | await async_client.update_config(config) 47 | -------------------------------------------------------------------------------- /tests/endpoints/test_cveallowlist.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from pytest_httpserver import HTTPServer 8 | 9 | from harborapi.client import HarborAsyncClient 10 | from harborapi.models import CVEAllowlist 11 | from harborapi.models import CVEAllowlistItem 12 | 13 | from ..strategies import cveallowlist_strategy 14 | 15 | 16 | @pytest.mark.asyncio 17 | @given(cveallowlist_strategy) 18 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 19 | async def test_get_cve_allowlist( 20 | async_client: HarborAsyncClient, 21 | httpserver: HTTPServer, 22 | cve_allowlist: CVEAllowlist, 23 | ): 24 | httpserver.expect_oneshot_request( 25 | "/api/v2.0/system/CVEAllowlist", 26 | method="GET", 27 | ).respond_with_json(cve_allowlist.model_dump(mode="json")) 28 | 29 | allowlist = await async_client.get_cve_allowlist() 30 | assert allowlist == cve_allowlist 31 | if allowlist.items: 32 | assert all(isinstance(i, CVEAllowlistItem) for i in allowlist.items) 33 | 34 | 35 | @pytest.mark.asyncio 36 | @given(cveallowlist_strategy) 37 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 38 | async def test_update_cve_allowlist( 39 | async_client: HarborAsyncClient, 40 | httpserver: HTTPServer, 41 | cve_allowlist: CVEAllowlist, 42 | ): 43 | # TODO: improve this test? We don't have a way to check the response body 44 | # when using .update_cve_allowlist(). Call ._put() directly? 45 | httpserver.expect_oneshot_request( 46 | "/api/v2.0/system/CVEAllowlist", 47 | method="PUT", 48 | ).respond_with_data() 49 | 50 | await async_client.update_cve_allowlist(cve_allowlist) 51 | -------------------------------------------------------------------------------- /tests/endpoints/test_gc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from typing import List 5 | 6 | import pytest 7 | from hypothesis import HealthCheck 8 | from hypothesis import given 9 | from hypothesis import settings 10 | from hypothesis import strategies as st 11 | from pytest_httpserver import HTTPServer 12 | 13 | from harborapi.client import HarborAsyncClient 14 | from harborapi.models.models import GCHistory 15 | from harborapi.models.models import Schedule 16 | 17 | from ..utils import json_from_list 18 | 19 | 20 | @pytest.mark.asyncio 21 | @given(st.builds(Schedule)) 22 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 23 | async def test_get_gc_schedule_mock( 24 | async_client: HarborAsyncClient, 25 | httpserver: HTTPServer, 26 | schedule: Schedule, 27 | ): 28 | httpserver.expect_oneshot_request( 29 | "/api/v2.0/system/gc/schedule", 30 | method="GET", 31 | ).respond_with_data(schedule.model_dump_json(), content_type="application/json") 32 | resp = await async_client.get_gc_schedule() 33 | assert resp == schedule 34 | 35 | 36 | @pytest.mark.asyncio 37 | @given(st.builds(Schedule)) 38 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 39 | async def test_create_gc_schedule_mock( 40 | async_client: HarborAsyncClient, 41 | httpserver: HTTPServer, 42 | schedule: Schedule, 43 | ): 44 | expect_location = "/api/v2.0/system/gc/schedule" # idk? 45 | 46 | httpserver.expect_oneshot_request( 47 | "/api/v2.0/system/gc/schedule", 48 | method="POST", 49 | json=json.loads(schedule.model_dump_json(exclude_unset=True)), 50 | ).respond_with_data( 51 | headers={"Location": expect_location}, 52 | status=201, 53 | ) 54 | location = await async_client.create_gc_schedule(schedule) 55 | assert location == expect_location 56 | 57 | 58 | @pytest.mark.asyncio 59 | @given(st.builds(Schedule)) 60 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 61 | async def test_update_gc_schedule_mock( 62 | async_client: HarborAsyncClient, 63 | httpserver: HTTPServer, 64 | schedule: Schedule, 65 | ): 66 | httpserver.expect_oneshot_request( 67 | "/api/v2.0/system/gc/schedule", 68 | method="PUT", 69 | json=schedule.model_dump(mode="json", exclude_unset=True), 70 | ).respond_with_data() 71 | await async_client.update_gc_schedule(schedule) 72 | 73 | 74 | @pytest.mark.asyncio 75 | @given(st.lists(st.builds(GCHistory))) 76 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 77 | async def test_get_gc_jobs_mock( 78 | async_client: HarborAsyncClient, 79 | httpserver: HTTPServer, 80 | jobs: List[GCHistory], 81 | ): 82 | httpserver.expect_oneshot_request( 83 | "/api/v2.0/system/gc", 84 | method="GET", 85 | ).respond_with_data( 86 | json_from_list(jobs), 87 | content_type="application/json", 88 | ) 89 | 90 | resp = await async_client.get_gc_jobs() 91 | assert resp == jobs 92 | 93 | 94 | @pytest.mark.asyncio 95 | @given(st.builds(GCHistory)) 96 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 97 | async def test_get_gc_job_mock( 98 | async_client: HarborAsyncClient, 99 | httpserver: HTTPServer, 100 | job: GCHistory, 101 | ): 102 | job.id = 123 103 | httpserver.expect_oneshot_request( 104 | "/api/v2.0/system/gc/123", 105 | method="GET", 106 | ).respond_with_data( 107 | job.model_dump_json(), 108 | content_type="application/json", 109 | ) 110 | 111 | resp = await async_client.get_gc_job(123) 112 | assert resp == job 113 | 114 | 115 | @pytest.mark.asyncio 116 | @pytest.mark.parametrize("as_list", [True, False]) 117 | @given(st.text()) 118 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 119 | async def test_get_gc_log_mock( 120 | async_client: HarborAsyncClient, 121 | httpserver: HTTPServer, 122 | as_list: bool, 123 | log: str, 124 | ): 125 | httpserver.expect_oneshot_request( 126 | "/api/v2.0/system/gc/123/log", 127 | method="GET", 128 | ).respond_with_data( 129 | log, 130 | content_type="text/plain", 131 | ) 132 | 133 | resp = await async_client.get_gc_log(123, as_list=as_list) 134 | if as_list: 135 | assert isinstance(resp, list) 136 | assert resp == log.splitlines() 137 | assert len(resp) == len(log.splitlines()) 138 | # any other reasonable assertions? 139 | else: 140 | assert resp == log 141 | -------------------------------------------------------------------------------- /tests/endpoints/test_health.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models import ComponentHealthStatus 12 | from harborapi.models import OverallHealthStatus 13 | 14 | 15 | @pytest.mark.asyncio 16 | @given(st.builds(OverallHealthStatus)) 17 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 18 | async def test_health_check( 19 | async_client: HarborAsyncClient, 20 | httpserver: HTTPServer, 21 | healthstatus: OverallHealthStatus, 22 | ): 23 | httpserver.expect_oneshot_request( 24 | "/api/v2.0/health", method="GET" 25 | ).respond_with_json(healthstatus.model_dump(mode="json")) 26 | 27 | health = await async_client.health_check() 28 | assert health == healthstatus 29 | if health.components: # TODO: add OverallHealthStatus.components strategy 30 | assert all(isinstance(i, ComponentHealthStatus) for i in health.components) 31 | -------------------------------------------------------------------------------- /tests/endpoints/test_icon.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models import Icon 12 | 13 | 14 | @pytest.mark.asyncio 15 | @given(st.builds(Icon)) 16 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 17 | async def test_get_icon_mock( 18 | async_client: HarborAsyncClient, 19 | httpserver: HTTPServer, 20 | icon: Icon, 21 | ): 22 | httpserver.expect_oneshot_request( 23 | "/api/v2.0/icons/digest", method="GET" 24 | ).respond_with_data(icon.model_dump_json(), content_type="application/json") 25 | 26 | resp = await async_client.get_icon("digest") 27 | assert resp.content_type == icon.content_type 28 | assert resp.content == icon.content 29 | -------------------------------------------------------------------------------- /tests/endpoints/test_immutable.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.models import ImmutableRule 14 | 15 | from ..utils import json_from_list 16 | 17 | 18 | @pytest.mark.asyncio 19 | @given(st.lists(st.builds(ImmutableRule))) 20 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 21 | async def test_get_project_immutable_tag_rules_mock( 22 | async_client: HarborAsyncClient, 23 | httpserver: HTTPServer, 24 | rules: List[ImmutableRule], 25 | ): 26 | httpserver.expect_oneshot_request( 27 | "/api/v2.0/projects/1234/immutabletagrules", method="GET" 28 | ).respond_with_data( 29 | json_from_list(rules), headers={"Content-Type": "application/json"} 30 | ) 31 | resp = await async_client.get_project_immutable_tag_rules(1234) 32 | assert resp == rules 33 | 34 | 35 | @pytest.mark.asyncio 36 | @given(st.builds(ImmutableRule)) 37 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 38 | async def test_create_project_immutable_tag_rule_mock( 39 | async_client: HarborAsyncClient, 40 | httpserver: HTTPServer, 41 | rule: ImmutableRule, 42 | ): 43 | expect_location = "/api/v2.0/projects/1234/immutabletagrules/1" 44 | httpserver.expect_oneshot_request( 45 | "/api/v2.0/projects/1234/immutabletagrules", 46 | method="POST", 47 | json=rule.model_dump(mode="json", exclude_unset=True), 48 | ).respond_with_data(headers={"Location": expect_location}) 49 | resp = await async_client.create_project_immutable_tag_rule(1234, rule) 50 | assert resp == expect_location 51 | 52 | 53 | @pytest.mark.asyncio 54 | @given(st.builds(ImmutableRule)) 55 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 56 | async def test_update_project_immutable_tag_rule_mock( 57 | async_client: HarborAsyncClient, 58 | httpserver: HTTPServer, 59 | rule: ImmutableRule, 60 | ): 61 | httpserver.expect_oneshot_request( 62 | "/api/v2.0/projects/1234/immutabletagrules/1", 63 | method="PUT", 64 | json=rule.model_dump(mode="json", exclude_unset=True), 65 | headers={"X-Is-Resource-Name": "false"}, 66 | ).respond_with_data() 67 | 68 | await async_client.update_project_immutable_tag_rule(1234, 1, rule) 69 | 70 | 71 | @pytest.mark.asyncio 72 | @pytest.mark.parametrize( 73 | "enable", 74 | [True, False], 75 | ) 76 | async def test_enable_project_immutable_tagrule( 77 | async_client: HarborAsyncClient, 78 | httpserver: HTTPServer, 79 | enable: bool, 80 | ): 81 | """Test updating a rule with only the disabled field set.""" 82 | httpserver.expect_oneshot_request( 83 | "/api/v2.0/projects/1234/immutabletagrules/1", 84 | method="PUT", 85 | json={"disabled": not enable}, 86 | headers={"X-Is-Resource-Name": "false"}, 87 | ).respond_with_data() 88 | 89 | await async_client.enable_project_immutable_tagrule(1234, 1, enable) 90 | 91 | 92 | @pytest.mark.asyncio 93 | async def test_delete_project_immutable_tag_rule_mock( 94 | async_client: HarborAsyncClient, 95 | httpserver: HTTPServer, 96 | ): 97 | httpserver.expect_oneshot_request( 98 | "/api/v2.0/projects/1234/immutabletagrules/1", 99 | method="DELETE", 100 | headers={"X-Is-Resource-Name": "false"}, 101 | ).respond_with_data() 102 | 103 | await async_client.delete_project_immutable_tag_rule(1234, 1) 104 | -------------------------------------------------------------------------------- /tests/endpoints/test_labels.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.models import Label 14 | 15 | from ..utils import json_from_list 16 | 17 | 18 | @pytest.mark.asyncio 19 | @given(st.builds(Label)) 20 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 21 | async def test_get_label_mock( 22 | async_client: HarborAsyncClient, 23 | httpserver: HTTPServer, 24 | label: Label, 25 | ): 26 | label_id = 123 27 | httpserver.expect_oneshot_request( 28 | f"/api/v2.0/labels/{label_id}", method="GET" 29 | ).respond_with_data( 30 | label.model_dump_json(), headers={"Content-Type": "application/json"} 31 | ) 32 | 33 | resp = await async_client.get_label(123) 34 | assert resp == label 35 | 36 | 37 | @pytest.mark.asyncio 38 | @given(st.builds(Label)) 39 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 40 | async def test_update_label_mock( 41 | async_client: HarborAsyncClient, 42 | httpserver: HTTPServer, 43 | label: Label, 44 | ): 45 | label_id = 123 46 | httpserver.expect_oneshot_request( 47 | f"/api/v2.0/labels/{label_id}", 48 | method="PUT", 49 | json=label.model_dump(mode="json", exclude_unset=True), 50 | ).respond_with_data() 51 | 52 | await async_client.update_label(123, label) 53 | 54 | 55 | @pytest.mark.asyncio 56 | @given(st.builds(Label)) 57 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 58 | async def test_create_label_mock( 59 | async_client: HarborAsyncClient, 60 | httpserver: HTTPServer, 61 | label: Label, 62 | ): 63 | label_id = 123 64 | expect_location = f"/api/v2.0/labels/{label_id}" 65 | httpserver.expect_oneshot_request( 66 | "/api/v2.0/labels", 67 | method="POST", 68 | json=label.model_dump(mode="json", exclude_unset=True), 69 | ).respond_with_data(headers={"Location": expect_location}) 70 | 71 | resp = await async_client.create_label(label) 72 | assert resp == expect_location 73 | 74 | 75 | @pytest.mark.asyncio 76 | async def test_delete_label_mock( 77 | async_client: HarborAsyncClient, 78 | httpserver: HTTPServer, 79 | ): 80 | label_id = 123 81 | httpserver.expect_oneshot_request( 82 | f"/api/v2.0/labels/{label_id}", method="DELETE" 83 | ).respond_with_data() 84 | 85 | await async_client.delete_label(label_id) 86 | 87 | 88 | @pytest.mark.asyncio 89 | @given(st.lists(st.builds(Label))) 90 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 91 | async def test_get_labels_mock( 92 | async_client: HarborAsyncClient, 93 | httpserver: HTTPServer, 94 | labels: List[Label], 95 | ): 96 | httpserver.expect_oneshot_request( 97 | "/api/v2.0/labels", method="GET" 98 | ).respond_with_data( 99 | json_from_list(labels), 100 | headers={"Content-Type": "application/json"}, 101 | ) 102 | 103 | # TODO: query parameters 104 | resp = await async_client.get_labels() 105 | assert resp == labels 106 | -------------------------------------------------------------------------------- /tests/endpoints/test_ldap.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | from typing import Optional 5 | 6 | import pytest 7 | from hypothesis import HealthCheck 8 | from hypothesis import given 9 | from hypothesis import settings 10 | from hypothesis import strategies as st 11 | from pytest_httpserver import HTTPServer 12 | 13 | from harborapi.client import HarborAsyncClient 14 | from harborapi.models.models import LdapConf 15 | from harborapi.models.models import LdapPingResult 16 | from harborapi.models.models import LdapUser 17 | from harborapi.models.models import UserGroup 18 | 19 | from ..utils import json_from_list 20 | 21 | 22 | @pytest.mark.asyncio 23 | @given(st.builds(LdapConf), st.builds(LdapPingResult)) 24 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 25 | async def test_ping_ldap_with_config_mock( 26 | async_client: HarborAsyncClient, 27 | httpserver: HTTPServer, 28 | config: LdapConf, 29 | result: LdapPingResult, 30 | ): 31 | httpserver.expect_oneshot_request( 32 | "/api/v2.0/ldap/ping", 33 | method="POST", 34 | json=config.model_dump(mode="json", exclude_unset=True), 35 | ).respond_with_data(result.model_dump_json(), content_type="application/json") 36 | 37 | resp = await async_client.ping_ldap(config) 38 | assert resp == result 39 | 40 | 41 | @pytest.mark.asyncio 42 | @given(st.builds(LdapPingResult)) 43 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 44 | async def test_ping_ldap_no_config_mock( 45 | async_client: HarborAsyncClient, 46 | httpserver: HTTPServer, 47 | result: LdapPingResult, 48 | ): 49 | httpserver.expect_oneshot_request( 50 | "/api/v2.0/ldap/ping", 51 | method="POST", 52 | ).respond_with_data(result.model_dump_json(), content_type="application/json") 53 | 54 | resp = await async_client.ping_ldap() 55 | assert resp == result 56 | 57 | 58 | @pytest.mark.asyncio 59 | @pytest.mark.parametrize( 60 | "group_name,group_dn", [("foo", "bar"), (None, "bar"), ("foo", None), (None, None)] 61 | ) 62 | @given(st.lists(st.builds(UserGroup))) 63 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 64 | async def test_search_ldap_groups_mock( 65 | async_client: HarborAsyncClient, 66 | httpserver: HTTPServer, 67 | group_name: Optional[str], 68 | group_dn: Optional[str], 69 | results: List[UserGroup], 70 | ): 71 | httpserver.expect_oneshot_request( 72 | "/api/v2.0/ldap/groups/search", 73 | method="GET", 74 | ).respond_with_data(json_from_list(results), content_type="application/json") 75 | 76 | if not group_name and not group_dn: 77 | with pytest.raises(ValueError): 78 | await async_client.search_ldap_groups(group_name, group_dn) 79 | else: 80 | resp = await async_client.search_ldap_groups(group_name, group_dn) 81 | assert resp == results 82 | 83 | 84 | @pytest.mark.asyncio 85 | @pytest.mark.parametrize("username", ["test-user", ""]) 86 | @given(st.lists(st.builds(LdapUser))) 87 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 88 | async def test_search_ldap_users_mock( 89 | async_client: HarborAsyncClient, 90 | httpserver: HTTPServer, 91 | username: str, 92 | results: List[LdapUser], 93 | ): 94 | httpserver.expect_oneshot_request( 95 | "/api/v2.0/ldap/users/search", 96 | method="GET", 97 | ).respond_with_data(json_from_list(results), content_type="application/json") 98 | 99 | resp = await async_client.search_ldap_users(username) 100 | assert resp == results 101 | 102 | 103 | @pytest.mark.asyncio 104 | @given(st.lists(st.text())) 105 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 106 | async def test_import_ldap_users_mock( 107 | async_client: HarborAsyncClient, 108 | httpserver: HTTPServer, 109 | user_ids: List[str], 110 | ): 111 | httpserver.expect_oneshot_request( 112 | "/api/v2.0/ldap/users/import", 113 | method="POST", 114 | json={"ldap_uid_list": user_ids}, 115 | ).respond_with_data() 116 | 117 | # The method constructs the request model, 118 | # we just pass in the list of user IDs. 119 | await async_client.import_ldap_users(user_ids) 120 | -------------------------------------------------------------------------------- /tests/endpoints/test_oidc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from contextlib import nullcontext 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.exceptions import StatusError 14 | from harborapi.models import OIDCTestReq 15 | 16 | 17 | @pytest.mark.asyncio 18 | @pytest.mark.parametrize("status", [200, 400, 401, 403, 404, 500]) 19 | @given(st.builds(OIDCTestReq)) 20 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 21 | async def test_test_oidc_mock( 22 | async_client: HarborAsyncClient, 23 | httpserver: HTTPServer, 24 | status: int, 25 | oidcreq: OIDCTestReq, 26 | ): 27 | httpserver.expect_oneshot_request( 28 | "/api/v2.0/system/oidc/ping", 29 | method="POST", 30 | json=oidcreq.model_dump(mode="json", exclude_unset=True), 31 | ).respond_with_data(status=status) 32 | 33 | if status == 200: 34 | ctx = nullcontext() 35 | else: 36 | ctx = pytest.raises(StatusError) 37 | with ctx as exc_info: 38 | await async_client.test_oidc(oidcreq=oidcreq) 39 | if status == 200: 40 | assert exc_info is None 41 | else: 42 | assert exc_info is not None 43 | assert exc_info.value.status_code == status 44 | assert exc_info.value.__cause__.response.status_code == status 45 | -------------------------------------------------------------------------------- /tests/endpoints/test_permissions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models.models import Permissions 12 | 13 | 14 | @pytest.mark.asyncio 15 | @given(st.builds(Permissions)) 16 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 17 | async def test_get_permissions( 18 | async_client: HarborAsyncClient, 19 | httpserver: HTTPServer, 20 | permissions: Permissions, 21 | ): 22 | httpserver.expect_oneshot_request( 23 | "/api/v2.0/permissions", 24 | method="GET", 25 | ).respond_with_json( 26 | permissions.model_dump(mode="json"), 27 | headers={"Content-Type": "application/json"}, 28 | ) 29 | 30 | resp = await async_client.get_permissions() 31 | assert resp == permissions 32 | -------------------------------------------------------------------------------- /tests/endpoints/test_ping.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from pytest_httpserver import HTTPServer 5 | 6 | from harborapi.client import HarborAsyncClient 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_ping( 11 | async_client: HarborAsyncClient, 12 | httpserver: HTTPServer, 13 | ): 14 | httpserver.expect_oneshot_request("/api/v2.0/ping").respond_with_data("pong") 15 | 16 | pong = await async_client.ping() 17 | assert pong == "pong" 18 | -------------------------------------------------------------------------------- /tests/endpoints/test_projectmetadata.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Dict 4 | from typing import Union 5 | 6 | import pytest 7 | from hypothesis import HealthCheck 8 | from hypothesis import given 9 | from hypothesis import settings 10 | from hypothesis import strategies as st 11 | from pytest_httpserver import HTTPServer 12 | 13 | from harborapi.client import HarborAsyncClient 14 | from harborapi.models.models import ProjectMetadata 15 | 16 | 17 | @pytest.mark.asyncio 18 | @given(st.builds(ProjectMetadata)) 19 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 20 | async def test_get_project_metadata_mock( 21 | async_client: HarborAsyncClient, 22 | httpserver: HTTPServer, 23 | project_name_or_id: Union[str, int], 24 | metadata: ProjectMetadata, 25 | ): 26 | httpserver.expect_oneshot_request( 27 | f"/api/v2.0/projects/{project_name_or_id}/metadatas", method="GET" 28 | ).respond_with_data(metadata.model_dump_json(), content_type="application/json") 29 | 30 | resp = await async_client.get_project_metadata(project_name_or_id) 31 | assert resp == metadata 32 | 33 | 34 | @pytest.mark.asyncio 35 | @given(st.builds(ProjectMetadata)) 36 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 37 | async def test_set_project_metadata_mock( 38 | async_client: HarborAsyncClient, 39 | httpserver: HTTPServer, 40 | project_name_or_id: Union[str, int], 41 | metadata: ProjectMetadata, 42 | ): 43 | httpserver.expect_oneshot_request( 44 | f"/api/v2.0/projects/{project_name_or_id}/metadatas", 45 | method="POST", 46 | json=metadata.model_dump(mode="json", exclude_unset=True), 47 | ).respond_with_data() 48 | 49 | await async_client.set_project_metadata(project_name_or_id, metadata) 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_get_project_metadata_entry_mock( 54 | async_client: HarborAsyncClient, 55 | httpserver: HTTPServer, 56 | project_name_or_id: Union[str, int], 57 | ): 58 | httpserver.expect_oneshot_request( 59 | f"/api/v2.0/projects/{project_name_or_id}/metadatas/auto_scan", method="GET" 60 | ).respond_with_json({"auto_scan": "true"}) 61 | 62 | resp = await async_client.get_project_metadata_entry( 63 | project_name_or_id, "auto_scan" 64 | ) 65 | assert resp == {"auto_scan": "true"} 66 | 67 | 68 | @pytest.mark.asyncio 69 | @pytest.mark.parametrize( 70 | "new_metadata", [ProjectMetadata(auto_scan="true"), {"auto_scan": "true"}] 71 | ) 72 | async def test_update_project_metadata_entry_mock( 73 | async_client: HarborAsyncClient, 74 | httpserver: HTTPServer, 75 | project_name_or_id: Union[str, int], 76 | new_metadata: Union[ProjectMetadata, Dict[str, str]], 77 | ): 78 | httpserver.expect_oneshot_request( 79 | f"/api/v2.0/projects/{project_name_or_id}/metadatas/auto_scan", method="PUT" 80 | ).respond_with_data() 81 | 82 | await async_client.update_project_metadata_entry( 83 | project_name_or_id, 84 | "auto_scan", 85 | new_metadata, 86 | ) 87 | 88 | 89 | async def test_update_project_metadata_with_extra_field_mock( 90 | async_client: HarborAsyncClient, 91 | httpserver: HTTPServer, 92 | project_name_or_id: Union[str, int], 93 | ): 94 | """Testing that we can instantiate ProjectMetadata with extra fields, 95 | and it will be serialized correctly when sending to the API.""" 96 | new_metadata = ProjectMetadata(foo="bar") 97 | httpserver.expect_oneshot_request( 98 | f"/api/v2.0/projects/{project_name_or_id}/metadatas/foo", method="PUT" 99 | ).respond_with_data() 100 | 101 | await async_client.update_project_metadata_entry( 102 | project_name_or_id, 103 | "foo", 104 | new_metadata, 105 | ) 106 | 107 | 108 | @pytest.mark.asyncio 109 | async def test_delete_project_metadata_entry_mock( 110 | async_client: HarborAsyncClient, 111 | httpserver: HTTPServer, 112 | project_name_or_id: Union[str, int], 113 | ): 114 | httpserver.expect_oneshot_request( 115 | f"/api/v2.0/projects/{project_name_or_id}/metadatas/foo", method="DELETE" 116 | ).respond_with_data() 117 | 118 | await async_client.delete_project_metadata_entry(project_name_or_id, "foo") 119 | -------------------------------------------------------------------------------- /tests/endpoints/test_quota.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.models import Quota 14 | from harborapi.models.models import QuotaUpdateReq 15 | from harborapi.models.models import ResourceList 16 | 17 | 18 | @pytest.mark.asyncio 19 | @given(st.lists(st.builds(Quota))) 20 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 21 | async def test_get_quotas_mock( 22 | async_client: HarborAsyncClient, 23 | httpserver: HTTPServer, 24 | quotas: List[Quota], 25 | ): 26 | httpserver.expect_oneshot_request( 27 | "/api/v2.0/quotas", method="GET" 28 | ).respond_with_json([q.model_dump(mode="json", exclude_unset=True) for q in quotas]) 29 | 30 | resp = await async_client.get_quotas() 31 | if quotas: 32 | assert resp == quotas 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_update_quota(async_client: HarborAsyncClient, httpserver: HTTPServer): 37 | httpserver.expect_oneshot_request( 38 | "/api/v2.0/quotas/1234", 39 | method="PUT", 40 | json={"hard": {"storage": 100, "storage2": 200}}, 41 | ).respond_with_data() 42 | 43 | await async_client.update_quota( 44 | 1234, 45 | QuotaUpdateReq( 46 | hard=ResourceList( 47 | storage=100, # type: ignore 48 | storage2=200, # type: ignore 49 | ) 50 | ), 51 | ) 52 | 53 | 54 | @pytest.mark.asyncio 55 | @given(st.builds(Quota)) 56 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 57 | async def test_get_quota_mock( 58 | async_client: HarborAsyncClient, 59 | httpserver: HTTPServer, 60 | quota: Quota, 61 | ): 62 | httpserver.expect_oneshot_request( 63 | "/api/v2.0/quotas/1234", method="GET" 64 | ).respond_with_json(quota.model_dump(mode="json", exclude_unset=True)) 65 | 66 | resp = await async_client.get_quota(1234) 67 | assert resp == quota 68 | -------------------------------------------------------------------------------- /tests/endpoints/test_repositories.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | from typing import Optional 5 | 6 | import pytest 7 | from hypothesis import HealthCheck 8 | from hypothesis import given 9 | from hypothesis import settings 10 | from hypothesis import strategies as st 11 | from pytest_httpserver import HTTPServer 12 | 13 | from harborapi.client import HarborAsyncClient 14 | from harborapi.models import Repository 15 | 16 | from ..utils import json_from_list 17 | 18 | 19 | @pytest.mark.asyncio 20 | @given(st.builds(Repository)) 21 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 22 | async def test_get_repository_mock( 23 | async_client: HarborAsyncClient, 24 | httpserver: HTTPServer, 25 | repository: Repository, 26 | ): 27 | httpserver.expect_oneshot_request( 28 | "/api/v2.0/projects/testproj/repositories/testrepo", method="GET" 29 | ).respond_with_data( 30 | repository.model_dump_json(), headers={"Content-Type": "application/json"} 31 | ) 32 | 33 | resp = await async_client.get_repository("testproj", "testrepo") 34 | assert resp == repository 35 | 36 | 37 | @pytest.mark.asyncio 38 | @given(st.builds(Repository)) 39 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 40 | async def test_update_repository_mock( 41 | async_client: HarborAsyncClient, 42 | httpserver: HTTPServer, 43 | repository: Repository, 44 | ): 45 | httpserver.expect_oneshot_request( 46 | "/api/v2.0/projects/testproj/repositories/testrepo", 47 | method="PUT", 48 | json=repository.model_dump(mode="json", exclude_unset=True), 49 | ).respond_with_data() 50 | 51 | await async_client.update_repository( 52 | "testproj", 53 | "testrepo", 54 | repository, 55 | ) 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_delete_repository_mock( 60 | async_client: HarborAsyncClient, 61 | httpserver: HTTPServer, 62 | ): 63 | httpserver.expect_oneshot_request( 64 | "/api/v2.0/projects/testproj/repositories/testrepo", method="DELETE" 65 | ).respond_with_data() 66 | 67 | await async_client.delete_repository("testproj", "testrepo") 68 | 69 | 70 | @pytest.mark.asyncio 71 | @given(st.lists(st.builds(Repository))) 72 | @pytest.mark.parametrize( 73 | "project_name,expected_url", 74 | [ 75 | ("testproj", "/api/v2.0/projects/testproj/repositories"), 76 | (None, "/api/v2.0/repositories"), 77 | ], 78 | ) 79 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 80 | async def test_get_repositories_mock( 81 | async_client: HarborAsyncClient, 82 | httpserver: HTTPServer, 83 | project_name: Optional[str], 84 | expected_url: str, 85 | repositories: List[Repository], 86 | ): 87 | httpserver.expect_oneshot_request(expected_url, method="GET").respond_with_data( 88 | json_from_list(repositories), 89 | headers={"Content-Type": "application/json"}, 90 | ) 91 | 92 | resp = await async_client.get_repositories(project_name) 93 | assert resp == repositories 94 | -------------------------------------------------------------------------------- /tests/endpoints/test_robot_v1.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.models.models import Robot 14 | from harborapi.models.models import RobotCreated 15 | from harborapi.models.models import RobotCreateV1 16 | 17 | from ..utils import json_from_list 18 | 19 | 20 | @pytest.mark.asyncio 21 | @given(st.builds(RobotCreateV1), st.builds(RobotCreated)) 22 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 23 | async def test_create_robot_v1_mock( 24 | async_client: HarborAsyncClient, 25 | httpserver: HTTPServer, 26 | robot: RobotCreateV1, 27 | robot_created: RobotCreated, 28 | ): 29 | httpserver.expect_oneshot_request( 30 | "/api/v2.0/projects/1234/robots", 31 | method="POST", 32 | json=robot.model_dump(mode="json", exclude_unset=True), 33 | ).respond_with_data( 34 | robot_created.model_dump_json(), content_type="application/json" 35 | ) 36 | resp = await async_client.create_robot_v1(1234, robot) 37 | assert resp == robot_created 38 | 39 | 40 | @pytest.mark.asyncio 41 | @given(st.lists(st.builds(Robot))) 42 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 43 | async def test_get_robots_v1_mock( 44 | async_client: HarborAsyncClient, 45 | httpserver: HTTPServer, 46 | robots: List[Robot], 47 | ): 48 | httpserver.expect_oneshot_request( 49 | "/api/v2.0/projects/1234/robots", method="GET" 50 | ).respond_with_data(json_from_list(robots), content_type="application/json") 51 | 52 | resp = await async_client.get_robots_v1(1234) 53 | assert resp == robots 54 | 55 | 56 | @pytest.mark.asyncio 57 | @given(st.builds(Robot)) 58 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 59 | async def test_get_robot_v1_mock( 60 | async_client: HarborAsyncClient, 61 | httpserver: HTTPServer, 62 | robot: Robot, 63 | ): 64 | httpserver.expect_oneshot_request( 65 | "/api/v2.0/projects/1234/robots/1", method="GET" 66 | ).respond_with_data(robot.model_dump_json(), content_type="application/json") 67 | 68 | resp = await async_client.get_robot_v1(1234, 1) 69 | assert resp == robot 70 | 71 | 72 | @pytest.mark.asyncio 73 | @given(st.builds(Robot)) 74 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 75 | async def test_update_robot_v1_mock( 76 | async_client: HarborAsyncClient, 77 | httpserver: HTTPServer, 78 | robot: Robot, 79 | ): 80 | httpserver.expect_oneshot_request( 81 | "/api/v2.0/projects/1234/robots/1", 82 | method="PUT", 83 | json=robot.model_dump(mode="json", exclude_unset=True), 84 | ).respond_with_data() 85 | 86 | await async_client.update_robot_v1(1234, 1, robot) 87 | 88 | 89 | @pytest.mark.asyncio 90 | async def test_delete_robot_v1_mock( 91 | async_client: HarborAsyncClient, 92 | httpserver: HTTPServer, 93 | ): 94 | httpserver.expect_oneshot_request( 95 | "/api/v2.0/projects/1234/robots/1", method="DELETE" 96 | ).respond_with_data() 97 | 98 | await async_client.delete_robot_v1(1234, 1) 99 | -------------------------------------------------------------------------------- /tests/endpoints/test_scan.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from pytest_httpserver import HTTPServer 5 | 6 | from harborapi.client import HarborAsyncClient 7 | from harborapi.utils import get_artifact_path 8 | 9 | 10 | @pytest.mark.asyncio 11 | @pytest.mark.parametrize("status_code", [200, 202]) 12 | async def test_scan_artifact_mock( 13 | async_client: HarborAsyncClient, 14 | httpserver: HTTPServer, 15 | caplog: pytest.LogCaptureFixture, 16 | status_code: int, 17 | ): 18 | project = "test-proj" 19 | repository = "test-repo" 20 | artifact = "test-artifact" 21 | # TODO: test "/" in repo name 22 | 23 | artifact_path = get_artifact_path(project, repository, artifact) 24 | endpoint_path = f"/api/v2.0{artifact_path}/scan" 25 | httpserver.expect_oneshot_request( 26 | endpoint_path, 27 | method="POST", 28 | ).respond_with_data("foo", status=status_code) 29 | 30 | await async_client.scan_artifact(project, repository, artifact) 31 | if status_code == 200: 32 | assert "expected 202" in caplog.text 33 | 34 | 35 | @pytest.mark.asyncio 36 | async def test_get_scan_report_log_mock( 37 | async_client: HarborAsyncClient, 38 | httpserver: HTTPServer, 39 | ): 40 | project = "test-proj" 41 | repository = "test-repo" 42 | artifact = "test-artifact" 43 | report_id = "bar" 44 | 45 | artifact_path = get_artifact_path(project, repository, artifact) 46 | endpoint_path = f"/api/v2.0{artifact_path}/scan/{report_id}/log" 47 | httpserver.expect_oneshot_request( 48 | endpoint_path, 49 | method="GET", 50 | ).respond_with_data(f"foo: {report_id}") 51 | 52 | resp = await async_client.get_artifact_scan_report_log( 53 | project, repository, artifact, report_id 54 | ) 55 | assert resp == "foo: bar" 56 | 57 | 58 | @pytest.mark.asyncio 59 | @pytest.mark.parametrize("status_code", [200, 202]) 60 | async def test_stop_artifact_scan_mock( 61 | async_client: HarborAsyncClient, 62 | httpserver: HTTPServer, 63 | caplog: pytest.LogCaptureFixture, 64 | status_code: int, 65 | ): 66 | project = "test-proj" 67 | repository = "test-repo" 68 | artifact = "test-artifact" 69 | # TODO: test "/" in repo name 70 | 71 | artifact_path = get_artifact_path(project, repository, artifact) 72 | endpoint_path = f"/api/v2.0{artifact_path}/scan/stop" 73 | httpserver.expect_oneshot_request( 74 | endpoint_path, 75 | method="POST", 76 | ).respond_with_data("foo", status=status_code) 77 | 78 | await async_client.stop_artifact_scan(project, repository, artifact) 79 | if status_code == 200: 80 | assert "expected 202" in caplog.text 81 | -------------------------------------------------------------------------------- /tests/endpoints/test_scanall.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models import Schedule 12 | from harborapi.models import Stats 13 | 14 | 15 | @pytest.mark.asyncio 16 | @given(st.builds(Stats)) 17 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 18 | async def test_get_scan_all_metrics_mock( 19 | async_client: HarborAsyncClient, 20 | httpserver: HTTPServer, 21 | stats: Stats, 22 | ): 23 | httpserver.expect_oneshot_request( 24 | "/api/v2.0/scans/all/metrics", method="GET" 25 | ).respond_with_json(stats.model_dump(mode="json", exclude_unset=True)) 26 | 27 | metrics = await async_client.get_scan_all_metrics() 28 | assert metrics is not None 29 | 30 | 31 | @pytest.mark.asyncio 32 | @given(st.builds(Schedule)) 33 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 34 | async def test_update_scan_all_schedule_mock( 35 | async_client: HarborAsyncClient, 36 | httpserver: HTTPServer, 37 | schedule: Schedule, 38 | ): 39 | # TODO: use st.lists(st.builds(ScannerRegistration)) to generate a list of scanners 40 | httpserver.expect_oneshot_request( 41 | "/api/v2.0/system/scanAll/schedule", method="PUT" 42 | ).respond_with_data() 43 | 44 | await async_client.update_scan_all_schedule( 45 | schedule 46 | ) # just test endpoint is working 47 | 48 | 49 | @pytest.mark.asyncio 50 | @given(st.builds(Schedule)) 51 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 52 | async def test_create_scan_all_schedule_mock( 53 | async_client: HarborAsyncClient, 54 | httpserver: HTTPServer, 55 | schedule: Schedule, 56 | ): 57 | # TODO: use st.lists(st.builds(ScannerRegistration)) to generate a list of scanners 58 | httpserver.expect_oneshot_request( 59 | "/api/v2.0/system/scanAll/schedule", method="POST" 60 | ).respond_with_data( 61 | status=201, headers={"Location": "/system/scanAll/schedules/1234"} 62 | ) 63 | 64 | resp = await async_client.create_scan_all_schedule(schedule) 65 | assert resp == "/system/scanAll/schedules/1234" 66 | 67 | 68 | @pytest.mark.asyncio 69 | @given(st.builds(Schedule)) 70 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 71 | async def test_get_scan_all_schedule( 72 | async_client: HarborAsyncClient, 73 | httpserver: HTTPServer, 74 | schedule: Schedule, 75 | ): 76 | # TODO: use st.lists(st.builds(ScannerRegistration)) to generate a list of scanners 77 | httpserver.expect_oneshot_request( 78 | "/api/v2.0/system/scanAll/schedule", method="GET" 79 | ).respond_with_json(schedule.model_dump(mode="json", exclude_unset=True)) 80 | 81 | schedule_resp = await async_client.get_scan_all_schedule() 82 | assert schedule_resp == schedule 83 | 84 | 85 | @pytest.mark.asyncio 86 | async def test_stop_scan_all_job( 87 | async_client: HarborAsyncClient, httpserver: HTTPServer 88 | ): 89 | httpserver.expect_oneshot_request( 90 | "/api/v2.0/system/scanAll/stop", method="POST" 91 | ).respond_with_data() 92 | 93 | await async_client.stop_scan_all_job() 94 | -------------------------------------------------------------------------------- /tests/endpoints/test_scanexport.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.exceptions import HarborAPIException 12 | from harborapi.models import ScanDataExportExecution 13 | from harborapi.models import ScanDataExportExecutionList 14 | from harborapi.models import ScanDataExportJob 15 | from harborapi.models import ScanDataExportRequest 16 | 17 | 18 | @pytest.mark.asyncio 19 | @given(st.builds(ScanDataExportExecutionList)) 20 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 21 | async def test_get_scan_exports_mock( 22 | async_client: HarborAsyncClient, 23 | httpserver: HTTPServer, 24 | exports: ScanDataExportExecutionList, 25 | ): 26 | httpserver.expect_oneshot_request( 27 | "/api/v2.0/export/cve/executions", 28 | method="GET", 29 | ).respond_with_data( 30 | exports.model_dump_json(), 31 | headers={"Content-Type": "application/json"}, 32 | ) 33 | 34 | resp = await async_client.get_scan_exports() 35 | assert resp == exports 36 | 37 | 38 | @pytest.mark.asyncio 39 | @given(st.builds(ScanDataExportExecution)) 40 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 41 | async def test_get_scan_export_mock( 42 | async_client: HarborAsyncClient, 43 | httpserver: HTTPServer, 44 | execution: ScanDataExportExecution, 45 | ): 46 | execution_id = 123 47 | httpserver.expect_oneshot_request( 48 | f"/api/v2.0/export/cve/execution/{execution_id}", 49 | method="GET", 50 | ).respond_with_data( 51 | execution.model_dump_json(), 52 | headers={"Content-Type": "application/json"}, 53 | ) 54 | 55 | resp = await async_client.get_scan_export(execution_id) 56 | assert resp == execution 57 | 58 | 59 | @pytest.mark.asyncio 60 | @given(st.builds(ScanDataExportRequest), st.builds(ScanDataExportJob)) 61 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 62 | async def test_export_scan_data_mock( 63 | async_client: HarborAsyncClient, 64 | httpserver: HTTPServer, 65 | request: ScanDataExportRequest, 66 | job: ScanDataExportJob, 67 | ): 68 | scan_type = "application/vnd.security.vulnerability.report; version=1.1" 69 | httpserver.expect_oneshot_request( 70 | "/api/v2.0/export/cve", 71 | method="POST", 72 | headers={"X-Scan-Data-Type": scan_type}, 73 | data=request.model_dump_json(exclude_unset=True), 74 | ).respond_with_data(job.model_dump_json(), content_type="application/json") 75 | 76 | resp = await async_client.export_scan_data(request, scan_type) 77 | assert resp == job 78 | 79 | 80 | @pytest.mark.asyncio 81 | @given(st.builds(ScanDataExportRequest)) 82 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 83 | async def test_export_scan_data_empty_response_mock( 84 | async_client: HarborAsyncClient, 85 | httpserver: HTTPServer, 86 | request: ScanDataExportRequest, 87 | ): 88 | scan_type = "application/vnd.security.vulnerability.report; version=1.1" 89 | httpserver.expect_oneshot_request( 90 | "/api/v2.0/export/cve", 91 | method="POST", 92 | headers={"X-Scan-Data-Type": scan_type}, 93 | data=request.model_dump_json(exclude_unset=True), 94 | ).respond_with_data("{}", content_type="application/json") 95 | 96 | with pytest.raises(HarborAPIException) as exc_info: 97 | await async_client.export_scan_data(request, scan_type) 98 | assert "empty response" in exc_info.value.args[0].lower() 99 | 100 | 101 | @pytest.mark.asyncio 102 | @given(st.binary()) 103 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 104 | async def test_download_scan_export_mock( 105 | async_client: HarborAsyncClient, httpserver: HTTPServer, data: bytes 106 | ): 107 | execution_id = 123 108 | httpserver.expect_oneshot_request( 109 | f"/api/v2.0/export/cve/download/{execution_id}", 110 | method="GET", 111 | ).respond_with_data(data) 112 | 113 | resp = await async_client.download_scan_export(execution_id) 114 | assert resp.content == data 115 | assert bytes(resp) == data 116 | -------------------------------------------------------------------------------- /tests/endpoints/test_search.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models import Search 12 | 13 | 14 | @pytest.mark.asyncio 15 | @given(st.builds(Search)) 16 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 17 | async def test_search_mock( 18 | async_client: HarborAsyncClient, 19 | httpserver: HTTPServer, 20 | search: Search, 21 | ): 22 | httpserver.expect_oneshot_request( 23 | "/api/v2.0/search", method="GET", query_string={"q": "testproj"} 24 | ).respond_with_data(search.model_dump_json(), content_type="application/json") 25 | 26 | resp = await async_client.search("testproj") 27 | assert resp == search 28 | 29 | 30 | def test_search_chart() -> None: 31 | """/search returning charts as a separate search result was deprecated in #18265 (https://github.com/goharbor/harbor/pull/18265) 32 | 33 | This test ensures that we can parse the response from /search even if 34 | it includes charts.""" 35 | 36 | data = { 37 | "chart": [ 38 | { 39 | "Chart": { 40 | "apiVersion": "v2", 41 | "appVersion": "1.23.1", 42 | "description": "NGINX Open Source is a web server that can be also used as a reverse proxy, load balancer, and HTTP cache. Recommended for high-demanding sites due to its ability to provide faster content.", 43 | "engine": None, 44 | "home": "https://github.com/bitnami/charts/tree/master/bitnami/nginx", 45 | "icon": "https://bitnami.com/assets/stacks/nginx/img/nginx-stack-220x234.png", 46 | "keywords": ["nginx", "http", "web", "www", "reverse proxy"], 47 | "name": "myproject/nginx", 48 | "sources": [ 49 | "https://github.com/bitnami/containers/tree/main/bitnami/nginx", 50 | "https://www.nginx.org", 51 | ], 52 | "version": "13.1.6", 53 | "created": "2023-02-03T09:38:19.867594256Z", 54 | "digest": "56663051192d296847e60ea81cebe03a26a703c3c6eef8f976509f80dc5e87ea", 55 | "urls": ["myproject/charts/nginx-13.1.6.tgz"], 56 | "labels": None, 57 | }, 58 | "Name": "myproject/nginx", 59 | } 60 | ], 61 | "project": [], 62 | "repository": [], 63 | } 64 | 65 | s = Search(**data) 66 | assert s.project == [] 67 | assert s.repository == [] 68 | # we accept extra fields for compatibility: 69 | assert s.chart == data["chart"] 70 | -------------------------------------------------------------------------------- /tests/endpoints/test_statistic.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models import Statistic 12 | 13 | 14 | @pytest.mark.asyncio 15 | @given(st.builds(Statistic)) 16 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 17 | async def test_get_statistics_mock( 18 | async_client: HarborAsyncClient, 19 | httpserver: HTTPServer, 20 | statistic: Statistic, 21 | ): 22 | httpserver.expect_oneshot_request( 23 | "/api/v2.0/statistics", method="GET" 24 | ).respond_with_json(statistic.model_dump(mode="json", exclude_unset=True)) 25 | 26 | resp = await async_client.get_statistics() 27 | assert resp == statistic 28 | -------------------------------------------------------------------------------- /tests/endpoints/test_systeminfo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | from hypothesis import strategies as st 8 | from pytest_httpserver import HTTPServer 9 | 10 | from harborapi.client import HarborAsyncClient 11 | from harborapi.models import SystemInfo 12 | from harborapi.models.models import GeneralInfo 13 | 14 | 15 | @pytest.mark.asyncio 16 | @given(st.builds(SystemInfo)) 17 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 18 | async def test_get_system_volume_info_mock( 19 | async_client: HarborAsyncClient, 20 | httpserver: HTTPServer, 21 | systeminfo: SystemInfo, 22 | ): 23 | httpserver.expect_oneshot_request( 24 | "/api/v2.0/systeminfo/volumes", method="GET" 25 | ).respond_with_json(systeminfo.model_dump(mode="json", exclude_unset=True)) 26 | 27 | resp = await async_client.get_system_volume_info() 28 | assert resp == systeminfo 29 | 30 | 31 | @pytest.mark.asyncio 32 | @given(st.builds(GeneralInfo)) 33 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 34 | async def test_get_system_info_mock( 35 | async_client: HarborAsyncClient, 36 | httpserver: HTTPServer, 37 | generalinfo: GeneralInfo, 38 | ): 39 | httpserver.expect_oneshot_request( 40 | "/api/v2.0/systeminfo", method="GET" 41 | ).respond_with_json(generalinfo.model_dump(mode="json", exclude_unset=True)) 42 | 43 | resp = await async_client.get_system_info() 44 | assert resp == generalinfo 45 | 46 | 47 | @pytest.mark.asyncio 48 | @given(st.binary()) 49 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 50 | async def test_get_system_certificate_mock( 51 | async_client: HarborAsyncClient, 52 | httpserver: HTTPServer, 53 | content: bytes, 54 | ): 55 | httpserver.expect_oneshot_request( 56 | "/api/v2.0/systeminfo/getcert", method="GET" 57 | ).respond_with_data(content) 58 | 59 | resp = await async_client.get_system_certificate() 60 | assert resp.content == content 61 | -------------------------------------------------------------------------------- /tests/endpoints/test_usergroups.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List 4 | 5 | import pytest 6 | from hypothesis import HealthCheck 7 | from hypothesis import given 8 | from hypothesis import settings 9 | from hypothesis import strategies as st 10 | from pytest_httpserver import HTTPServer 11 | 12 | from harborapi.client import HarborAsyncClient 13 | from harborapi.models.models import UserGroup 14 | from harborapi.models.models import UserGroupSearchItem 15 | 16 | from ..utils import json_from_list 17 | 18 | 19 | @pytest.mark.asyncio 20 | @given(st.lists(st.builds(UserGroupSearchItem))) 21 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 22 | async def test_search_usergroups_mock( 23 | async_client: HarborAsyncClient, 24 | httpserver: HTTPServer, 25 | usergroups: List[UserGroupSearchItem], 26 | ): 27 | httpserver.expect_oneshot_request( 28 | "/api/v2.0/usergroups/search", 29 | method="GET", 30 | query_string={"groupname": "test", "page": "1", "page_size": "10"}, 31 | ).respond_with_data(json_from_list(usergroups), content_type="application/json") 32 | resp = await async_client.search_usergroups("test") 33 | assert resp == usergroups 34 | 35 | 36 | @pytest.mark.asyncio 37 | @given(st.builds(UserGroup)) 38 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 39 | async def test_create_usergroup_mock( 40 | async_client: HarborAsyncClient, 41 | httpserver: HTTPServer, 42 | usergroup: UserGroup, 43 | ): 44 | usergroup.id = 123 45 | expect_location = "/api/v2.0/usergroups/123" # idk? 46 | 47 | httpserver.expect_oneshot_request( 48 | "/api/v2.0/usergroups", 49 | method="POST", 50 | json=usergroup.model_dump(mode="json", exclude_unset=True), 51 | ).respond_with_data( 52 | headers={"Location": expect_location}, 53 | status=201, 54 | ) 55 | location = await async_client.create_usergroup(usergroup) 56 | assert location == expect_location 57 | 58 | 59 | @pytest.mark.asyncio 60 | @given(st.builds(UserGroup)) 61 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 62 | async def test_update_usergroup_mock( 63 | async_client: HarborAsyncClient, 64 | httpserver: HTTPServer, 65 | usergroup: UserGroup, 66 | ): 67 | httpserver.expect_oneshot_request( 68 | "/api/v2.0/usergroups/123", 69 | method="PUT", 70 | json=usergroup.model_dump(mode="json", exclude_unset=True), 71 | ).respond_with_data() 72 | await async_client.update_usergroup(123, usergroup) 73 | 74 | 75 | @pytest.mark.asyncio 76 | @given(st.lists(st.builds(UserGroup))) 77 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 78 | async def test_get_usergroups_mock( 79 | async_client: HarborAsyncClient, 80 | httpserver: HTTPServer, 81 | usergroups: List[UserGroup], 82 | ): 83 | httpserver.expect_oneshot_request( 84 | "/api/v2.0/usergroups", 85 | method="GET", 86 | ).respond_with_data( 87 | json_from_list(usergroups), 88 | content_type="application/json", 89 | ) 90 | 91 | resp = await async_client.get_usergroups() 92 | assert resp == usergroups 93 | 94 | 95 | @pytest.mark.asyncio 96 | @given(st.builds(UserGroup)) 97 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 98 | async def test_get_usergroup_mock( 99 | async_client: HarborAsyncClient, 100 | httpserver: HTTPServer, 101 | usergroup: UserGroup, 102 | ): 103 | usergroup.id = 123 104 | httpserver.expect_oneshot_request( 105 | "/api/v2.0/usergroups/123", 106 | method="GET", 107 | ).respond_with_data( 108 | usergroup.model_dump_json(), 109 | content_type="application/json", 110 | ) 111 | 112 | resp = await async_client.get_usergroup(123) 113 | assert resp == usergroup 114 | 115 | 116 | @pytest.mark.asyncio 117 | async def test_delete_usergroup( 118 | async_client: HarborAsyncClient, 119 | httpserver: HTTPServer, 120 | ): 121 | httpserver.expect_oneshot_request( 122 | "/api/v2.0/usergroups/123", 123 | method="DELETE", 124 | ).respond_with_data() 125 | 126 | await async_client.delete_usergroup(123) 127 | -------------------------------------------------------------------------------- /tests/ext/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/tests/ext/__init__.py -------------------------------------------------------------------------------- /tests/ext/test_artifact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | from hypothesis import HealthCheck 5 | from hypothesis import given 6 | from hypothesis import settings 7 | 8 | from harborapi.ext.artifact import ArtifactInfo 9 | from harborapi.models import Tag 10 | from harborapi.models.scanner import Severity 11 | from harborapi.models.scanner import VulnerabilityItem 12 | from harborapi.version import SemVer 13 | 14 | from ..strategies.ext import artifact_info_strategy 15 | 16 | 17 | @given(artifact_info_strategy) 18 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 19 | def test_artifactinfo( 20 | artifact: ArtifactInfo, 21 | ) -> None: 22 | vuln = VulnerabilityItem( 23 | id="CVE-2022-test-1", 24 | package="test-package", 25 | version="1.0.0", 26 | fix_version="1.0.1", 27 | severity=Severity.high, 28 | description="test description", 29 | links=[ 30 | "https://www.test.com", 31 | "https://www.test2.com", 32 | ], 33 | ) 34 | artifact.report.vulnerabilities = [vuln] 35 | 36 | assert artifact.artifact.tags 37 | artifact.artifact.tags[0].name = "latest-test" 38 | assert artifact.has_tag("latest-test") 39 | 40 | assert artifact.has_cve("CVE-2022-test-1") 41 | assert artifact.has_description("test description") 42 | # regex 43 | assert artifact.has_cve("CVE-2022-test-.*") 44 | assert artifact.has_description(".*description") 45 | 46 | # Affected package (with and without version constraints) 47 | assert artifact.has_package("test-package") 48 | assert artifact.has_package("test-package", min_version=(1, 0, 0)) 49 | assert artifact.has_package("test-package", max_version=(1, 0, 0)) 50 | assert artifact.has_package("test-package", max_version=(1, 0, 1)) 51 | assert not artifact.has_package("test-package", min_version=(1, 0, 1)) 52 | assert not artifact.has_package("test-package", max_version=(0, 9, 1)) 53 | # regex 54 | assert artifact.has_package("test-.*", min_version=(1, 0, 0)) 55 | assert artifact.has_package("Test-.*", min_version=(1, 0, 0)) 56 | assert not artifact.has_package( 57 | "Test-.*", case_sensitive=True, min_version=(1, 0, 0) 58 | ) 59 | 60 | # Different types of version constraints 61 | for version in [(1, 0, 0), "1.0.0", 1, SemVer(1, 0, 0)]: 62 | assert artifact.has_package("test-package", min_version=version) 63 | assert artifact.has_package( 64 | "test-package", min_version=version, max_version=version 65 | ) 66 | 67 | # Invalid version constraint (min > max) 68 | with pytest.raises(ValueError): 69 | assert artifact.has_package( 70 | "test-package", max_version=(1, 0, 0), min_version=(1, 0, 1) 71 | ) 72 | 73 | # CVE that doesn't exist 74 | assert not artifact.has_cve("CVE-2022-test-2") 75 | assert not artifact.has_description("test description 2") 76 | assert not artifact.has_package("test-package-2") 77 | # regex 78 | assert not artifact.has_package(".*package-2") 79 | 80 | vuln2 = vuln.model_copy(deep=True) 81 | vuln2.id = "CVE-2022-test-2" 82 | vuln2.description = None 83 | artifact.report.vulnerabilities.append(vuln2) 84 | 85 | assert artifact.vuln_with_cve("CVE-2022-test-1") == vuln 86 | assert list(artifact.vulns_with_package("test-package")) == [vuln, vuln2] 87 | assert list(artifact.vulns_with_description("test description")) == [vuln] 88 | # regex 89 | assert artifact.has_cve(".*2022-test-2") 90 | assert artifact.cvss is not None 91 | assert artifact.cvss.max >= 0 92 | assert artifact.cvss.min >= 0 93 | assert artifact.cvss.mean >= 0 94 | assert artifact.cvss.median >= 0 95 | assert artifact.cvss.stdev >= 0 96 | 97 | # Properties 98 | artifact.repository.name = "test-project/test-repo" 99 | artifact.artifact.digest = "sha256:1234567890abcdef" 100 | artifact.artifact.tags = [Tag(name="test-tag")] 101 | assert artifact.project_name == "test-project" 102 | assert artifact.repository_name == "test-repo" 103 | assert artifact.name_with_digest == "test-project/test-repo@sha256:12345678" 104 | assert artifact.name_with_tag == "test-project/test-repo:test-tag" 105 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_file.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import httpx 4 | 5 | from harborapi.models.file import FileResponse 6 | 7 | 8 | def test_file_response() -> None: 9 | response = httpx.Response( 10 | 200, 11 | content=b"Hello, World!", 12 | headers={"content-type": "text/plain"}, 13 | default_encoding="utf-8", 14 | ) 15 | file_response = FileResponse(response) 16 | assert file_response.content == b"Hello, World!" 17 | assert file_response.encoding == "utf-8" 18 | assert file_response.content_type == "text/plain" 19 | assert file_response.headers == { 20 | "content-type": "text/plain", 21 | "content-length": "13", # added by httpx 22 | } 23 | assert bytes(file_response) == b"Hello, World!" 24 | -------------------------------------------------------------------------------- /tests/models/test_mappings.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unioslo/harborapi/dea9cafea35736b5b6a52fc1947b08fe2db08a40/tests/models/test_mappings.py -------------------------------------------------------------------------------- /tests/models/test_models_compat.py: -------------------------------------------------------------------------------- 1 | """Tests for models modules with a leading underscore in their name. 2 | 3 | In previous versions of harborapi, models were defined in modules with a leading 4 | underscore in their name, and then fixes were applied in models with the same 5 | name without the leading underscore. In order to maintain backwards compatibility, 6 | the underscore modules are still present, but they are deprecated and will be removed 7 | in a future version. 8 | """ 9 | 10 | from __future__ import annotations 11 | 12 | import pytest 13 | 14 | # These tests could be flaky! Not sure. 15 | # We just import both modules and check that the underscore modules contain 16 | # the same classes as the non-underscore modules. 17 | 18 | 19 | def test_models_import() -> None: 20 | from harborapi.models import models 21 | 22 | # Importing the underscore module should emit a DeprecationWarning 23 | with pytest.deprecated_call(): 24 | from harborapi.models import _models 25 | 26 | for model_name in dir(models): 27 | if model_name.startswith("_"): 28 | continue 29 | if not model_name[0].isupper(): 30 | continue 31 | assert getattr(models, model_name) == getattr(_models, model_name) 32 | 33 | 34 | def test_scanner_import() -> None: 35 | from harborapi.models import scanner 36 | 37 | with pytest.deprecated_call(): 38 | from harborapi.models import _scanner 39 | 40 | for model_name in dir(scanner): 41 | if model_name.startswith("_"): 42 | continue 43 | if not model_name[0].isupper(): 44 | continue 45 | assert getattr(scanner, model_name) == getattr(_scanner, model_name) 46 | -------------------------------------------------------------------------------- /tests/models/test_projectmetadata.py: -------------------------------------------------------------------------------- 1 | """The ProjectMetadata model spec specifies that all fields are strings, 2 | but their valid values are 'true' and 'false'. 3 | 4 | This module tests our validator that converts bools to the strings 'true' 5 | and 'false' instead of 'True' and 'False'. 6 | """ 7 | 8 | from __future__ import annotations 9 | 10 | from harborapi.models import ProjectMetadata 11 | 12 | 13 | def test_project_metadata_bool_converter() -> None: 14 | p = ProjectMetadata( 15 | public=True, 16 | enable_content_trust=True, 17 | enable_content_trust_cosign=True, 18 | prevent_vul=True, 19 | severity="high", 20 | auto_scan=False, 21 | reuse_sys_cve_allowlist=True, 22 | retention_id="7", 23 | ) 24 | 25 | assert p.public == "true" 26 | assert p.enable_content_trust == "true" 27 | assert p.enable_content_trust_cosign == "true" 28 | assert p.prevent_vul == "true" 29 | assert p.severity == "high" 30 | assert p.auto_scan == "false" 31 | assert p.reuse_sys_cve_allowlist == "true" 32 | assert p.retention_id == "7" 33 | 34 | 35 | def test_project_metadata_bool_converter_none() -> None: 36 | p = ProjectMetadata() 37 | assert p.public is None 38 | assert p.enable_content_trust is None 39 | assert p.enable_content_trust_cosign is None 40 | assert p.prevent_vul is None 41 | assert p.severity is None 42 | assert p.auto_scan is None 43 | assert p.reuse_sys_cve_allowlist is None 44 | assert p.retention_id is None 45 | 46 | 47 | def test_project_metadata_bool_converter_assignment() -> None: 48 | p = ProjectMetadata() 49 | assert p.public is None 50 | p.public = False 51 | assert p.public == "false" 52 | 53 | t = ProjectMetadata(public=False) 54 | assert p.public == "false" 55 | t.public = True 56 | assert t.public == "true" 57 | -------------------------------------------------------------------------------- /tests/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from hypothesis import strategies as st 4 | from hypothesis.provisional import urls 5 | from pydantic import AnyUrl 6 | 7 | from .artifact import artifact_strategy 8 | from .artifact import get_hbv_strategy 9 | from .artifact import get_vulnerability_item_strategy 10 | from .artifact import scanner_strategy 11 | from .cveallowlist import cveallowlist_strategy 12 | from .cveallowlist import cveallowlistitem_strategy 13 | from .errors import error_strategy 14 | from .errors import errors_strategy 15 | 16 | # TODO: make sure we generate None for Optional fields as well! 17 | 18 | 19 | def init_strategies() -> None: 20 | st.register_type_strategy(AnyUrl, urls()) 21 | -------------------------------------------------------------------------------- /tests/strategies/artifact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from hypothesis import strategies as st 4 | 5 | from harborapi.models.models import Accessory 6 | from harborapi.models.models import AdditionLinks 7 | from harborapi.models.models import Annotations 8 | from harborapi.models.models import Artifact 9 | from harborapi.models.models import ExtraAttrs 10 | from harborapi.models.models import Label 11 | from harborapi.models.models import ScanOverview 12 | from harborapi.models.models import Tag 13 | from harborapi.models.scanner import Artifact as ScanArtifact 14 | from harborapi.models.scanner import HarborVulnerabilityReport 15 | from harborapi.models.scanner import Scanner 16 | from harborapi.models.scanner import Severity 17 | from harborapi.models.scanner import VulnerabilityItem 18 | 19 | tag_strategy = st.builds( 20 | Tag, 21 | id=st.integers(), 22 | repository_id=st.integers(), 23 | artifact_id=st.integers(), 24 | name=st.text(), 25 | immutable=st.booleans(), 26 | signed=st.booleans(), 27 | ) 28 | 29 | artifact_strategy = st.builds( 30 | Artifact, 31 | id=st.integers(), 32 | type=st.one_of(st.sampled_from(["image", "chart"]), st.text()), 33 | # TODO: investiate proper values for this field 34 | manifest_media_type=st.sampled_from( 35 | ["application/vnd.docker.distribution.manifest.v2+json"] 36 | ), 37 | # TODO: add other possible mime types 38 | media_type=st.sampled_from( 39 | ["application/vnd.docker.distribution.manifest.v2+json"] 40 | ), 41 | project_id=st.integers(), 42 | repository_id=st.integers(), 43 | digest=st.text(), 44 | size=st.integers(), 45 | icon=st.one_of(st.text(), st.none()), 46 | annotations=st.builds(Annotations), 47 | extra_attrs=st.builds(ExtraAttrs), 48 | tags=st.lists(tag_strategy, min_size=1), 49 | addition_links=st.builds(AdditionLinks), 50 | labels=st.lists(st.builds(Label)), 51 | scan_overview=st.one_of(st.builds(ScanOverview), st.none()), 52 | accessories=st.lists(st.builds(Accessory)), 53 | ) 54 | artifact_or_none_strategy = st.one_of(st.none(), artifact_strategy) 55 | 56 | scanner_trivy_strategy = st.builds( 57 | Scanner, 58 | name=st.just("Trivy"), 59 | vendor=st.just("Aqua Security"), 60 | version=st.text(), 61 | ) 62 | 63 | # TODO: test with other scanners + random values 64 | # We rely on using "Trivy" as the scanner name in certain tests 65 | # Especially in tests/models/test_scanner.py 66 | # This is because the scanner name is used to retrieve the CVSS score 67 | # from the vulnerability item. 68 | scanner_strategy = st.one_of( 69 | st.none(), 70 | scanner_trivy_strategy, 71 | ) 72 | 73 | 74 | def get_vulnerability_item_strategy() -> st.SearchStrategy[VulnerabilityItem]: 75 | return st.builds( 76 | VulnerabilityItem, 77 | id=st.text(), 78 | package=st.text(), 79 | version=st.text(), # should major.minor.patch 80 | fix_version=st.text(), 81 | severity=st.sampled_from(Severity), 82 | ) 83 | 84 | 85 | def get_hbv_strategy() -> st.SearchStrategy[HarborVulnerabilityReport]: 86 | # TODO: add other possible mime types 87 | # TODO: add parameter for CVSS type to pass to get_vulnerability_item_strategy 88 | return st.builds( 89 | HarborVulnerabilityReport, 90 | artifact=st.builds(ScanArtifact), 91 | scanner=scanner_strategy, 92 | vulnerabilities=st.lists(get_vulnerability_item_strategy()), 93 | ) 94 | -------------------------------------------------------------------------------- /tests/strategies/cveallowlist.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from hypothesis import strategies as st 4 | 5 | from harborapi.models import CVEAllowlist 6 | from harborapi.models import CVEAllowlistItem 7 | 8 | cveallowlistitem_strategy = st.builds( 9 | CVEAllowlistItem, 10 | # TODO: make a that generates plausible CVE IDs 11 | cve_id=st.text(), 12 | ) 13 | 14 | 15 | cveallowlist_strategy = st.builds( 16 | CVEAllowlist, 17 | id=st.integers(), 18 | expires_at=st.one_of(st.integers(0, 2147483647), st.none()), 19 | items=st.lists(cveallowlistitem_strategy), 20 | creation_time=st.none(), 21 | update_time=st.none(), 22 | # creation_time=st.datetimes(), 23 | # update_time=st.datetimes(), 24 | ) 25 | -------------------------------------------------------------------------------- /tests/strategies/errors.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from hypothesis import strategies as st 4 | 5 | from harborapi.models import Error 6 | from harborapi.models import Errors 7 | 8 | error_strategy = st.builds( 9 | Error, 10 | code=st.text(), 11 | message=st.text(), 12 | ) 13 | errors_strategy = st.builds(Errors, errors=st.lists(error_strategy)) 14 | -------------------------------------------------------------------------------- /tests/strategies/ext.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from hypothesis import strategies as st 4 | 5 | from harborapi.ext.artifact import ArtifactInfo 6 | from harborapi.ext.report import ArtifactReport 7 | from harborapi.models.models import Repository 8 | 9 | from .artifact import artifact_strategy 10 | from .artifact import get_hbv_strategy 11 | 12 | artifact_info_strategy = st.builds( 13 | ArtifactInfo, 14 | artifact=artifact_strategy, 15 | repository=st.builds(Repository, name=st.text()), 16 | report=get_hbv_strategy(), 17 | ) 18 | 19 | artifact_report_strategy = st.builds( 20 | ArtifactReport, 21 | artifacts=st.lists(artifact_info_strategy, min_size=1), 22 | ) 23 | -------------------------------------------------------------------------------- /tests/test_auth.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from datetime import datetime 5 | from datetime import timezone 6 | from pathlib import Path 7 | 8 | import pytest 9 | from hypothesis import HealthCheck 10 | from hypothesis import given 11 | from hypothesis import settings 12 | from hypothesis import strategies as st 13 | 14 | from harborapi.auth import HarborAuthFile 15 | from harborapi.auth import load_harbor_auth_file 16 | from harborapi.auth import save_authfile 17 | from harborapi.models.models import Access 18 | from harborapi.models.models import RobotPermission 19 | 20 | 21 | def test_load_harbor_auth_file(credentials_file: Path): 22 | auth_file = load_harbor_auth_file(credentials_file) 23 | assert auth_file.id == 1 24 | assert auth_file.level == "system" 25 | assert auth_file.description == "Some description" 26 | assert auth_file.duration == 30 27 | assert auth_file.editable is True 28 | assert auth_file.expires_at == 1659273646 29 | assert auth_file.secret == "bad-password" 30 | 31 | assert auth_file.permissions == [ 32 | RobotPermission( 33 | access=[ 34 | Access(action="list", resource="repository"), 35 | Access(action="pull", resource="repository"), 36 | ], 37 | kind="project", 38 | namespace="*", 39 | ), 40 | ] 41 | assert auth_file.creation_time == datetime( 42 | 2022, 7, 1, 13, 20, 46, 230000, tzinfo=timezone.utc 43 | ) 44 | assert auth_file.update_time == datetime( 45 | 2022, 7, 6, 13, 26, 45, 360000, tzinfo=timezone.utc 46 | ) 47 | 48 | 49 | @pytest.mark.parametrize("field", ["name", "secret"]) 50 | def test_load_harbor_auth_file_exceptions( 51 | field: str, tmp_path: Path, credentials_dict: dict 52 | ): 53 | # Remove a field from the credentials_dict 54 | credentials_file = tmp_path / "credentials.json" 55 | del credentials_dict[field] 56 | credentials_file.write_text(json.dumps(credentials_dict)) 57 | 58 | # Assert the missing field is caught 59 | with pytest.raises(ValueError) as exc_info: 60 | load_harbor_auth_file(credentials_file) 61 | assert str(exc_info.value.args[0]) == f"Field '{field}' is required" 62 | 63 | 64 | @given(st.builds(HarborAuthFile)) 65 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 66 | def test_save_authfile(tmp_path: Path, auth_file: HarborAuthFile): 67 | # make sure auth_file has name and secret 68 | auth_file.name = "test" 69 | auth_file.secret = "test" 70 | fpath = tmp_path / "credentials.json" 71 | save_authfile(fpath, auth_file, overwrite=True) 72 | assert fpath.read_text() == auth_file.model_dump_json(indent=4) # potentially flaky 73 | assert load_harbor_auth_file(fpath) == auth_file 74 | 75 | with pytest.raises(FileExistsError) as exc_info: 76 | save_authfile(fpath, auth_file, overwrite=False) 77 | assert str(exc_info.value.args[0]) == f"File {fpath} already exists" 78 | -------------------------------------------------------------------------------- /tests/test_client_sync.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | 5 | from hypothesis import HealthCheck 6 | from hypothesis import given 7 | from hypothesis import settings 8 | from hypothesis import strategies as st 9 | from pytest_httpserver import HTTPServer 10 | 11 | from harborapi import HarborClient 12 | from harborapi.models import UserResp 13 | 14 | 15 | # NOTE: this will likely be removed in the future in favor of auto-generated 16 | # sync client tests. 17 | @given(st.builds(UserResp)) 18 | @settings(suppress_health_check=[HealthCheck.function_scoped_fixture]) 19 | def test_get_users_mock_sync(httpserver: HTTPServer, user: UserResp): 20 | # manually set version to v2.0 for this test 21 | 22 | httpserver.expect_oneshot_request( 23 | "/api/v2.0/users/1234", 24 | method="GET", 25 | ).respond_with_data(user.model_dump_json(), content_type="application/json") 26 | client = HarborClient( 27 | url=httpserver.url_for("/api/v2.0"), 28 | username="username", 29 | secret="secret", 30 | loop=asyncio.new_event_loop(), 31 | ) 32 | resp = client.get_user(1234) 33 | assert user == resp 34 | -------------------------------------------------------------------------------- /tests/test_responselog.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pytest_httpserver import HTTPServer 4 | 5 | from harborapi import HarborAsyncClient 6 | 7 | 8 | async def test_response_log(async_client: HarborAsyncClient, httpserver: HTTPServer): 9 | """Tests handling of data from the server that does not match the schema.""" 10 | httpserver.expect_request("/api/v2.0/users").respond_with_json( 11 | [{"username": "user1"}, {"username": "user2"}], status=200 12 | ) 13 | 14 | await async_client.get_users() 15 | await async_client.get_users() 16 | await async_client.get_users() 17 | assert len(async_client.response_log) == 3 18 | # check default maxlen 19 | assert async_client.response_log.entries.maxlen is None 20 | 21 | # test iteration + indexing 22 | for i, response in enumerate(async_client.response_log): 23 | assert response == async_client.response_log[i] 24 | assert response.status_code == 200 25 | # ignore params in case our defaults change in the future 26 | base_url = str(response.url).split("?")[0] 27 | assert base_url == async_client.url + "/users" 28 | assert response.method == "GET" 29 | assert response.response_size > 0 30 | 31 | # Resize log down to a smaller max size 32 | oldest_response = async_client.response_log[0] 33 | async_client.response_log.resize(2) 34 | assert len(async_client.response_log) == 2 35 | assert oldest_response not in async_client.response_log 36 | 37 | # Resize up to a larger max size 38 | async_client.response_log.resize(5) 39 | assert len(async_client.response_log) == 2 40 | 41 | # Clear the log 42 | async_client.response_log.clear() 43 | assert len(async_client.response_log) == 0 44 | assert async_client.response_log.entries.maxlen == 5 45 | 46 | 47 | async def test_last_response(async_client: HarborAsyncClient, httpserver: HTTPServer): 48 | """Test retrieving the last logged response.""" 49 | httpserver.expect_oneshot_request("/api/v2.0/users").respond_with_json( 50 | [{"username": "user1"}, {"username": "user2"}], status=200 51 | ) 52 | 53 | await async_client.get_users() 54 | last_response = async_client.last_response 55 | assert last_response is not None 56 | assert last_response.status_code == 200 57 | assert last_response.url == async_client.url + "/users?page=1&page_size=10" 58 | assert last_response.method == "GET" 59 | assert last_response.response_size > 0 60 | async_client.response_log.clear() 61 | assert async_client.last_response is None 62 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Sequence 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | def json_from_list(models: Sequence[BaseModel]) -> str: 9 | """Creates a JSON string from a list of BaseModel objects. 10 | We use this to deal with missing support for datetime serialization 11 | in pytest-httpserver. 12 | """ 13 | return "[" + ",".join(m.model_dump_json() for m in models) + "]" 14 | --------------------------------------------------------------------------------