diff --git a/README.rst b/README.rst index 8e53feb2..458df837 100644 --- a/README.rst +++ b/README.rst @@ -178,24 +178,16 @@ Look at the new Repository Version created } -Publish a Repository Version and create a Publication ------------------------------------------------------ +Add a Docker Distribution to serve the latest Repository Version +---------------------------------------------------------------- -``$ http POST :24817/pulp/api/v3/docker/publish/ repository=$REPO_HREF`` - -.. code:: json - - { - "task": "/pulp/api/v3/tasks/fd4cbecd-6c6a-4197-9cbe-4e45b0516309/" - } - -``$ export PUBLICATION_HREF=$(http :24817/pulp/api/v3/publications/ | jq -r '.results[0] | ._href')`` - -Add a Docker Distribution to serve your publication ---------------------------------------------------- - -``$ http POST http://localhost:24817/pulp/api/v3/docker-distributions/ name='baz' base_path='foo' publication=$PUBLICATION_HREF`` +The Docker Distribution will serve the latest version of a Repository if the repository is +specified during creation/update of a Docker Distribution. The Docker Distribution will serve +a specific repository version if repository_version is provided when creating a Docker +Distribution. Either repository or repository_version can be set on a Docker Distribution, but not +both. +``$ http POST http://localhost:24817/pulp/api/v3/docker-distributions/ name='baz' base_path='foo' repository=$REPO_HREF`` .. code:: json diff --git a/pulp_docker/app/models.py b/pulp_docker/app/models.py index a2546b8a..50cfe057 100644 --- a/pulp_docker/app/models.py +++ b/pulp_docker/app/models.py @@ -4,7 +4,7 @@ from django.db import models from pulpcore.plugin.download import DownloaderFactory -from pulpcore.plugin.models import BaseDistribution, Content, Remote +from pulpcore.plugin.models import BaseDistribution, Content, Remote, RepositoryVersion from . import downloaders @@ -287,5 +287,18 @@ class DockerDistribution(BaseDistribution): A docker distribution defines how a publication is distributed by Pulp's webserver. """ + repository_version = models.ForeignKey(RepositoryVersion, null=True, on_delete=models.CASCADE) + class Meta: default_related_name = 'docker_distributions' + + def get_repository_version(self): + """ + Returns the repository version that is supposed to be served by this DockerDistribution. + """ + if self.repository: + return RepositoryVersion.latest(self.repository) + elif self.repository_version: + return self.repository_version + else: + return None diff --git a/pulp_docker/app/registry.py b/pulp_docker/app/registry.py index 3914d83d..5abdbd1f 100644 --- a/pulp_docker/app/registry.py +++ b/pulp_docker/app/registry.py @@ -122,7 +122,8 @@ async def tags_list(request): path = request.match_info['path'] distribution = await Registry.match_distribution(path) tags = {'name': path, 'tags': set()} - for c in distribution.publication.repository_version.content: + repository_version = distribution.get_repository_version() + for c in repository_version.content: c = c.cast() if isinstance(c, ManifestTag) or isinstance(c, ManifestListTag): tags['tags'].add(c.name) @@ -149,11 +150,12 @@ async def get_tag(request): path = request.match_info['path'] tag_name = request.match_info['tag_name'] distribution = await Registry.match_distribution(path) + repository_version = distribution.get_repository_version() accepted_media_types = await Registry.get_accepted_media_types(request) if MEDIA_TYPE.MANIFEST_LIST in accepted_media_types: try: tag = ManifestListTag.objects.get( - pk__in=distribution.publication.repository_version.content, + pk__in=repository_version.content, name=tag_name ) # If there is no manifest list tag, try again with manifest tag. @@ -166,7 +168,7 @@ async def get_tag(request): if MEDIA_TYPE.MANIFEST_V2 in accepted_media_types: try: tag = ManifestTag.objects.get( - pk__in=distribution.publication.repository_version.content, + pk__in=repository_version.content, name=tag_name ) except ObjectDoesNotExist: @@ -211,11 +213,11 @@ async def get_by_digest(request): path = request.match_info['path'] digest = "sha256:{digest}".format(digest=request.match_info['digest']) distribution = await Registry.match_distribution(path) + repository_version = distribution.get_repository_version() log.info(digest) try: - ca = ContentArtifact.objects.get( - content__in=distribution.publication.repository_version.content, - relative_path=digest) + ca = ContentArtifact.objects.get(content__in=repository_version.content, + relative_path=digest) headers = {'Content-Type': ca.content.cast().media_type} except ObjectDoesNotExist: raise PathNotResolved(path) diff --git a/pulp_docker/app/serializers.py b/pulp_docker/app/serializers.py index a88925e0..0068bf87 100644 --- a/pulp_docker/app/serializers.py +++ b/pulp_docker/app/serializers.py @@ -10,6 +10,7 @@ BaseDistributionSerializer, DetailRelatedField, IdentityField, + NestedRelatedField, RemoteSerializer, SingleArtifactContentSerializer, ) @@ -182,8 +183,7 @@ class DockerDistributionSerializer(BaseDistributionSerializer): view_name='docker-distributions-detail' ) base_path = serializers.CharField( - help_text=_('The base (relative) path component of the published url. Avoid paths that \ - overlap with other distribution base paths (e.g. "foo" and "foo/bar")'), + help_text=_('The base (relative) path that identifies the registry path.'), validators=[validators.MaxLengthValidator( models.DockerDistribution._meta.get_field('base_path').max_length, message=_('Distribution base_path length must be less than {} characters').format( @@ -197,7 +197,68 @@ class DockerDistributionSerializer(BaseDistributionSerializer): help_text=_('The Registry hostame:port/name/ to use with docker pull command defined by ' 'this distribution.') ) + repository_version = NestedRelatedField( + help_text=_('A URI of the repository version to be served by the Docker Distribution.'), + required=False, + label=_('Repository Version'), + queryset=models.RepositoryVersion.objects.all(), + view_name='versions-detail', + lookup_field='number', + parent_lookup_kwargs={'repository_pk': 'repository__pk'}, + ) + + def validate(self, data): + """ + Validate the parameters for creating or updating Docker Distribution. + + This method makes sure that only repository or a repository version is associated with a + Docker Distribution. It also validates that the base_path is a relative path. + + Args: + data (dict): Dictionary of parameter value to validate + + Returns: + Dict of validated data + + Raises: + ValidationError if any of the validations fail. + + """ + super().validate(data) + if 'repository' in data: + repository = data['repository'] + elif self.instance: + repository = self.instance.repository + else: + repository = None + + if 'repository_version' in data: + repository_version = data['repository_version'] + elif self.instance: + repository_version = self.instance.repository_version + else: + repository_version = None + + if repository and repository_version: + raise serializers.ValidationError({'repository': _("Repository can't be set if " + "repository_version is set also.")}) + if 'publication' in data and data['publication']: + raise serializers.ValidationError({'publication': _("DockerDistributions don't serve " + "publications. A repository or " + "repository version should be " + "specified.")}) + if 'publisher' in data and data['publisher']: + raise serializers.ValidationError({'publication': _("DockerDistributions don't work " + "with publishers. A repository or " + "a repository version should be " + "specified.")}) + if 'base_path' in data and data['base_path']: + self._validate_relative_path(data['base_path']) + + return data class Meta: model = models.DockerDistribution - fields = BaseDistributionSerializer.Meta.fields + ('base_path', 'registry_path') + fields = BaseDistributionSerializer.Meta.fields + ('base_path', + 'registry_path', + 'repository_version') diff --git a/pulp_docker/app/tasks/__init__.py b/pulp_docker/app/tasks/__init__.py index d51020c8..27fd140c 100644 --- a/pulp_docker/app/tasks/__init__.py +++ b/pulp_docker/app/tasks/__init__.py @@ -1,3 +1,2 @@ from .distribution import create, delete, update # noqa -from .publishing import publish # noqa from .synchronize import synchronize # noqa diff --git a/pulp_docker/app/tasks/publishing.py b/pulp_docker/app/tasks/publishing.py deleted file mode 100644 index c5db9ece..00000000 --- a/pulp_docker/app/tasks/publishing.py +++ /dev/null @@ -1,25 +0,0 @@ -import logging -from gettext import gettext as _ - -from pulpcore.plugin.models import ( # noqa - RepositoryVersion, - Publication -) - - -log = logging.getLogger(__name__) - - -def publish(repository_version_pk): - """ - Create a Publication based on a RepositoryVersion. - - Args: - repository_version_pk (str): Create a publication from this repository version. - """ - repository_version = RepositoryVersion.objects.get(pk=repository_version_pk) - - with Publication.create(repository_version, pass_through=True) as publication: - pass - - log.info(_('Publication: {publication} created').format(publication=publication.pk)) diff --git a/pulp_docker/app/viewsets.py b/pulp_docker/app/viewsets.py index 0343c6ab..be91720a 100644 --- a/pulp_docker/app/viewsets.py +++ b/pulp_docker/app/viewsets.py @@ -10,11 +10,9 @@ from pulpcore.plugin.serializers import ( AsyncOperationResponseSerializer, - RepositoryPublishURLSerializer, RepositorySyncURLSerializer, ) -from pulpcore.plugin.models import RepositoryVersion, Publication from pulpcore.plugin.tasking import enqueue_with_reservation from pulpcore.plugin.viewsets import ( ContentViewSet, @@ -149,44 +147,6 @@ def sync(self, request, pk): return OperationPostponedResponse(result, request) -class DockerPublicationViewSet(NamedModelViewSet, mixins.CreateModelMixin): - """ - A ViewSet for Docker Publication. - """ - - endpoint_name = 'docker/publish' - queryset = Publication.objects.all() - serializer_class = RepositoryPublishURLSerializer - - @swagger_auto_schema( - operation_description="Trigger an asynchronous task to create a docker publication", - responses={202: AsyncOperationResponseSerializer} - ) - def create(self, request): - """ - Queues a task that publishes a new Docker Publication. - - """ - serializer = RepositoryPublishURLSerializer( - data=request.data, - context={'request': request} - ) - serializer.is_valid(raise_exception=True) - repository_version = serializer.validated_data.get('repository_version') - - # Safe because version OR repository is enforced by serializer. - if not repository_version: - repository = serializer.validated_data.get('repository') - repository_version = RepositoryVersion.latest(repository) - - result = enqueue_with_reservation( - tasks.publish, - [repository_version.repository], - kwargs={'repository_version_pk': str(repository_version.pk)} - ) - return OperationPostponedResponse(result, request) - - class DockerDistributionViewSet(NamedModelViewSet, mixins.UpdateModelMixin, mixins.RetrieveModelMixin, diff --git a/pulp_docker/tests/functional/api/test_publish.py b/pulp_docker/tests/functional/api/test_publish.py deleted file mode 100644 index 025ce493..00000000 --- a/pulp_docker/tests/functional/api/test_publish.py +++ /dev/null @@ -1,80 +0,0 @@ -# coding=utf-8 -"""Tests that publish docker plugin repositories.""" -import unittest -from random import choice - -from pulp_smash import api, config -from pulp_smash.pulp3.constants import REPO_PATH -from pulp_smash.pulp3.utils import ( - gen_repo, - get_content, - get_versions, - sync, -) - -from pulp_docker.tests.functional.utils import gen_docker_remote -from pulp_docker.tests.functional.constants import ( - DOCKER_CONTENT_NAME, - DOCKER_REMOTE_PATH, - DOCKER_PUBLICATION_PATH, -) -from pulp_docker.tests.functional.utils import set_up_module as setUpModule # noqa:F401 - - -class PublishAnyRepoVersionTestCase(unittest.TestCase): - """Test whether a particular repository version can be published. - - This test targets the following issues: - - * `Pulp #3324 `_ - * `Pulp Smash #897 `_ - """ - - @classmethod - def setUpClass(cls): - """Create class-wide variables.""" - cls.cfg = config.get_config() - cls.client = api.Client(cls.cfg, api.json_handler) - - def test_all(self): - """Test whether a particular repository version can be published. - - 1. Create a repository with at least 2 repository versions. - 2. Create a publication by supplying the latest ``repository_version``. - 3. Assert that the publication ``repository_version`` attribute points - to the latest repository version. - 4. Create a publication by supplying the non-latest ``repository_version``. - 5. Assert that the publication ``repository_version`` attribute points - to the supplied repository version. - """ - body = gen_docker_remote() - remote = self.client.post(DOCKER_REMOTE_PATH, body) - self.addCleanup(self.client.delete, remote['_href']) - - repo = self.client.post(REPO_PATH, gen_repo()) - self.addCleanup(self.client.delete, repo['_href']) - - sync(self.cfg, remote, repo) - - # Step 1 - repo = self.client.get(repo['_href']) - for docker_content in get_content(repo)[DOCKER_CONTENT_NAME]: - self.client.post( - repo['_versions_href'], - {'add_content_units': [docker_content['_href']]} - ) - version_hrefs = tuple(ver['_href'] for ver in get_versions(repo)) - non_latest = choice(version_hrefs[:-1]) - - # Step 2 - publication1 = self.client.using_handler(api.task_handler).post( - DOCKER_PUBLICATION_PATH, {"repository": repo["_href"]}) - # Step 3 - self.assertEqual(publication1['repository_version'], version_hrefs[-1]) - - # Step 4 - publication2 = self.client.using_handler(api.task_handler).post( - DOCKER_PUBLICATION_PATH, {"repository_version": non_latest}) - - # Step 5 - self.assertEqual(publication2['repository_version'], non_latest) diff --git a/pulp_docker/tests/functional/api/test_pull_content.py b/pulp_docker/tests/functional/api/test_pull_content.py index 0b7f3746..88cdb35d 100644 --- a/pulp_docker/tests/functional/api/test_pull_content.py +++ b/pulp_docker/tests/functional/api/test_pull_content.py @@ -22,7 +22,6 @@ DOCKER_CONTENT_NAME, DOCKER_DISTRIBUTION_PATH, DOCKER_REMOTE_PATH, - DOCKER_PUBLICATION_PATH, DOCKER_UPSTREAM_NAME, DOCKER_UPSTREAM_TAG, ) @@ -39,8 +38,8 @@ def setUpClass(cls): 1. Create a repository. 2. Create a remote pointing to external registry. 3. Sync the repository using the remote and re-read the repo data. - 4. Create a publication. - 5. Create a docker distribution to serve the publication. + 4. Create a docker distribution to serve the repository + 5. Create another docker distribution to the serve the repository version This tests targets the following issue: @@ -70,19 +69,26 @@ def setUpClass(cls): sync(cls.cfg, cls.remote, _repo) cls.repo = cls.client.get(_repo['_href']) - # Step 4 - cls.publication = cls.client.using_handler(api.task_handler).post( - DOCKER_PUBLICATION_PATH, {"repository": _repo['_href']}) + # Step 4. + response_dict = cls.client.using_handler(api.task_handler).post( + DOCKER_DISTRIBUTION_PATH, + gen_distribution(repository=cls.repo['_href']) + ) + distribution_href = response_dict['_href'] + cls.distribution_with_repo = cls.client.get(distribution_href) + cls.teardown_cleanups.append( + (cls.client.delete, cls.distribution_with_repo['_href']) + ) # Step 5. response_dict = cls.client.using_handler(api.task_handler).post( DOCKER_DISTRIBUTION_PATH, - gen_distribution(publication=cls.publication['_href']) + gen_distribution(repository_version=cls.repo['_latest_version_href']) ) distribution_href = response_dict['_href'] - cls.distribution = cls.client.get(distribution_href) + cls.distribution_with_repo_version = cls.client.get(distribution_href) cls.teardown_cleanups.append( - (cls.client.delete, cls.distribution['_href']) + (cls.client.delete, cls.distribution_with_repo_version['_href']) ) # remove callback if everything goes well @@ -119,7 +125,38 @@ def test_api_returns_same_checksum(self): 'Cannot find a matching layer on remote registry.' ) - def test_pull_image(self): + def test_pull_image_from_repository(self): + """Verify that a client can pull the image from Pulp. + + 1. Using the RegistryClient pull the image from Pulp. + 2. Pull the same image from remote registry. + 3. Verify both images has the same checksum. + 4. Ensure image is deleted after the test. + """ + registry = cli.RegistryClient(self.cfg) + registry.raise_if_unsupported( + unittest.SkipTest, 'Test requires podman/docker' + ) + + local_url = urljoin( + self.cfg.get_content_host_base_url(), + self.distribution_with_repo['base_path'] + ) + + registry.pull(local_url) + self.teardown_cleanups.append((registry.rmi, local_url)) + local_image = registry.inspect(local_url) + + registry.pull(DOCKER_UPSTREAM_NAME) + remote_image = registry.inspect(DOCKER_UPSTREAM_NAME) + + self.assertEqual( + local_image[0]['Id'], + remote_image[0]['Id'] + ) + registry.rmi(DOCKER_UPSTREAM_NAME) + + def test_pull_image_from_repository_version(self): """Verify that a client can pull the image from Pulp. 1. Using the RegistryClient pull the image from Pulp. @@ -134,7 +171,7 @@ def test_pull_image(self): local_url = urljoin( self.cfg.get_content_host_base_url(), - self.distribution['base_path'] + self.distribution_with_repo_version['base_path'] ) registry.pull(local_url) @@ -142,13 +179,13 @@ def test_pull_image(self): local_image = registry.inspect(local_url) registry.pull(DOCKER_UPSTREAM_NAME) - self.teardown_cleanups.append((registry.rmi, DOCKER_UPSTREAM_NAME)) remote_image = registry.inspect(DOCKER_UPSTREAM_NAME) self.assertEqual( - local_image[0]['Digest'], - remote_image[0]['Digest'] + local_image[0]['Id'], + remote_image[0]['Id'] ) + registry.rmi(DOCKER_UPSTREAM_NAME) def test_pull_image_with_tag(self): """Verify that a client can pull the image from Pulp with a tag. @@ -165,7 +202,7 @@ def test_pull_image_with_tag(self): local_url = urljoin( self.cfg.get_content_host_base_url(), - self.distribution['base_path'] + self.distribution_with_repo['base_path'] ) + DOCKER_UPSTREAM_TAG registry.pull(local_url) @@ -181,15 +218,15 @@ def test_pull_image_with_tag(self): ) self.assertEqual( - local_image[0]['Digest'], - remote_image[0]['Digest'] + local_image[0]['Id'], + remote_image[0]['Id'] ) - def test_pull_inexistent_image(self): - """Verify that a client cannot pull inexistent image from Pulp. + def test_pull_nonexistent_image(self): + """Verify that a client cannot pull nonexistent image from Pulp. - 1. Using the RegistryClient try to pull inexistent image from Pulp. - 2. Assert that error is occured and nothing has been pulled. + 1. Using the RegistryClient try to pull nonexistent image from Pulp. + 2. Assert that error is occurred and nothing has been pulled. """ registry = cli.RegistryClient(self.cfg) registry.raise_if_unsupported( diff --git a/pulp_docker/tests/functional/constants.py b/pulp_docker/tests/functional/constants.py index dc5117b5..7f0301c1 100644 --- a/pulp_docker/tests/functional/constants.py +++ b/pulp_docker/tests/functional/constants.py @@ -14,7 +14,6 @@ DOCKER_CONTENT_NAME = 'docker.manifest-blob' DOCKER_DISTRIBUTION_PATH = urljoin(BASE_PATH, 'docker-distributions/') -DOCKER_PUBLICATION_PATH = urljoin(BASE_PATH, 'docker/publish/') DOCKER_REMOTE_PATH = urljoin(BASE_REMOTE_PATH, 'docker/docker/')