Skip to content

Create/update

Methods that create and update resources make use of Pydantic models that have been generated from the official Harbor REST API Swagger schema.

The endpoint methods themselves have no parameters beyond the single model instance that is passed as the request body. This is done so that models can be updated in the future without breaking the methods that use them. We are at all times beholden to the official Swagger schema, and the models are generated from that schema. To see how to disable this validation and pass arbitrary data to the API, see the Validation page.

Create

Creating resources is done by calling one of the create_* methods on the client object. The model type expected for these methods is usually subtly different from the ones returned by get_* methods, and generally has the suffix *Req (e.g. ProjectReq instead of Project).

import asyncio
from harborapi import HarborAsyncClient
from harborapi.models import ProjectReq, ProjectMetadata

client = HarborAsyncClient(...)


async def main() -> None:
    project_path = await client.create_project(
        ProjectReq(
            project_name="test-project2",
            metadata=ProjectMetadata(
                public=True,
            ),
        )
    )
    print(f"Project created: {project_path}")


asyncio.run(main())

Update

The various update_* methods on the client object expect a *Req model similar to the create_* methods. However, one important distinction is that these methods also expect one or more identifiers for the resource to update the as the first argument(s) and then the model as the following argument:

client.update_project("name-of-project", ProjectReq(...))

Generally, only a single identifier is required to uniquely identify the resource to update, but some endpoints require multiple identifiers, such as the update_project_member_role method which expects both a project name/ID and a member ID:

import asyncio
from harborapi import HarborAsyncClient
from harborapi.models import ProjectReq, ProjectMetadata

client = HarborAsyncClient(...)


async def main() -> None:
    # Update the project
    await client.update_project(
        "test-project",
        ProjectReq(
            metadata=ProjectMetadata(
                enable_content_trust=True,
            ),
        ),
    )


asyncio.run(main())

In a departure from traditional REST semantics, the API updates only the fields that are explicitly defined on the model instance, preserving the undefined values as-is. This approach, while not aligning with the idiomatic use of HTTP PUT requests in REST, provides a measure of convenience from a user's perspective, since it allows us to update only the fields we care about, and leave the rest unchanged.

In the example, we only set the metadata.enable_content_trust field on the ProjectReq model, which means that only that one setting will be updated on the project. The rest of the project settings will be left unchanged.

See the Idiomatic REST updating section for more information on why this might not be the correct way to do things, and why it could change in the future if Harbor changes the API.

Idiomatic REST updating

Note

The following sections are optional reading, and are only relevant if you want to following idiomatic REST principles when updating resources. If you don't care about that, you can safely ignore the following sections.

The update endpoints are HTTP PUT endpoints that should expect a full resource definition according to RFC 72311. However, as described above, testing has shown that the API supports updating with partial models. By default, harborapi will only include the fields that are present in the request model, and leave out the rest, which enables us to take advantage of this behavior in the API:

from harborapi.models import ProjectReq, ProjectMetadata

project = ProjectReq(
    public=True,
    metadata=ProjectMetadata(
        auto_scan=True,
    ),
)

Will send the following over the wire, where unset fields are excluded:

{
  "public": true,
  "metadata": {
    "auto_scan": "true"
  }
}

Even though the internal representation of the model is:

ProjectReq(
    project_name=None,
    public=True,
    metadata=ProjectMetadata(
        public=None,
        enable_content_trust=None,
        enable_content_trust_cosign=None,
        prevent_vul=None,
        severity=None,
        auto_scan='true',
        reuse_sys_cve_allowlist=None,
        retention_id=None
    ),
    cve_allowlist=None,
    storage_limit=None,
    registry_id=None
)

This is because Pydantic knows which fields have been set and which have not on the model instance, and only serializes the fields that have been set. This way, we ensure that we don't send any data the user hasn't explicitly set.

Note

The reason for "auto_scan": "true" instead of "auto_scan": true can be found here.

Despite this behavior, it might a good idea to pass the full resource definition to the update_* methods, as the support for partial updates through the API may change in the future independently of this library without notice. The following sections demonstrate how to do this.

Converting GET models to PUT models

As outlined in Update, update_* methods expect subtly different models from the ones returned by get_* methods. By using the method convert_to() which is available on all models, we can easily convert a model we receive from a get_* method to the model type that the corresponding update_* method expects.

The method expects a model type as its first argument, and returns an instance of that model type: ../../reference/index.md'

from harborapi.models import Project, ProjectReq

project = Project(...)
req = project.convert_to(ProjectReq)
assert isinstance(req, ProjectReq)

print(req.metadata) # OK
print(req.owner_id) # FAIL - ProjectReq does not have an owner_id field

Note

The extra parameter is mainly available to ensure compatibility with future API changes, so unless you know you need to use it, you can safely ignore it.

If we want to, we can also pass in extra=True to include all fields of the original model on converted model, even if they are not defined in the schema of the model type we are converting to:

from harborapi.models import Project, ProjectReq

project = Project(owner_id=1, ...)
req = project.convert_to(ProjectReq, extra=True)
assert isinstance(req, ProjectReq)

print(req.metadata) # OK
print(req.owner_id) # OK - Inherited from Project via extra=True

Even though the ProjectReq model does not have an owner_id field in its schema, the resulting model instance received it from the Project model that it was converted from because we used convert_to(..., extra=True).

Updating a resource using convert_to()

Below is an example demonstrating how to fetch the existing resource (Project) and convert it to the model type the update method expects (ProjectReq):

import asyncio
from harborapi import HarborAsyncClient
from harborapi.models import ProjectReq

client = HarborAsyncClient(...)

async def main() -> None:
    # Get the project
    project = await client.get_project("test-project")

    # Convert to ProjectReq
    req = project.convert_to(ProjectReq)

    # Change the field we want to update
    req.metadata.enable_content_trust = True

    # Update the project
    await client.update_project("test-project", req)


asyncio.run(main())

  1. You can defend this behavior with certain interpretations of this quote from the RFC: "When a PUT representation is inconsistent with the target resource, the origin server SHOULD either make them consistent, by transforming the representation or changing the resource configuration [...]". However, this implicit behavior is not documented anywhere by Harbor, so we have no way of knowing if it is intentional or not.