diff --git a/CHANGES/1038.feature b/CHANGES/1038.feature new file mode 100644 index 000000000..2444ee209 --- /dev/null +++ b/CHANGES/1038.feature @@ -0,0 +1,2 @@ +Added the ``architectures`` and ``components`` parameters to the release API in order to create releases with accompanying architectures and components in a single API call. +Also added retrieve functionality to the release API. diff --git a/CHANGES/1038.removal b/CHANGES/1038.removal new file mode 100644 index 000000000..d1ad2794e --- /dev/null +++ b/CHANGES/1038.removal @@ -0,0 +1,3 @@ +When creating a release, the API now returns a task instead of the release being created. +In addition, attempting to create a release that already exists will no longer return an error. +Instead the resulting task will simply list the existing content in its ``created_resources`` field. diff --git a/docs/_scripts/structured_repo.sh b/docs/_scripts/structured_repo.sh index 1c39259df..fd39fe1e1 100755 --- a/docs/_scripts/structured_repo.sh +++ b/docs/_scripts/structured_repo.sh @@ -26,15 +26,13 @@ REPO_HREF=$(http ${PULP_URL}/pulp/api/v3/repositories/deb/apt/ name=${NAME} | jq TASK_HREF=$(http ${PULP_URL}/pulp/api/v3/distributions/deb/apt/ name=${NAME} base_path=${NAME} repository=${REPO_HREF} | jq -r .task) wait_until_task_finished ${PULP_URL}${TASK_HREF} -# upload the package to create Package, ReleaseComponent, PackageReleaseComponent, and Architecture content and add it to the repo in a single action -TASK_HREF=$(http --form ${PULP_URL}/pulp/api/v3/content/deb/packages/ file@frigg_1.0_ppc64.deb repository=${REPO_HREF} distribution=${DIST} component=${COMP} | jq -r .task) +# Create a Release, ReleaseArchitecture, and ReleaseComponent content to set various release file fields and add them to the repo in a single action +TASK_HREF=$(http ${PULP_URL}/pulp/api/v3/content/deb/releases/ repository=${REPO_HREF} distribution=${DIST} ${RELEASE_FILE_FIELDS[@]} architectures:='["amd64", "ppc64"]' components:=['"'${COMP}'"'] | jq -r .task) wait_until_task_finished ${PULP_URL}${TASK_HREF} -# Also create a Release content to set various release file fields -RELEASE_HREF=$(http ${PULP_URL}/pulp/api/v3/content/deb/releases/ distribution=${DIST} ${RELEASE_FILE_FIELDS[@]} | jq -r .pulp_href) - -# add our content to the repository -TASK_HREF=$(http ${PULP_URL}${REPO_HREF}modify/ add_content_units:="[\"${RELEASE_HREF}\"]" | jq -r .task) +# upload the package to create Package and PackageReleaseComponent content and add it to the repo in a single action +# the ReleaseComponent and ReleaseArchitecture were created in the previous step but could have been created in this step +TASK_HREF=$(http --form ${PULP_URL}/pulp/api/v3/content/deb/packages/ file@frigg_1.0_ppc64.deb repository=${REPO_HREF} distribution=${DIST} component=${COMP} | jq -r .task) wait_until_task_finished ${PULP_URL}${TASK_HREF} # publish our repo diff --git a/pulp_deb/app/serializers/content_serializers.py b/pulp_deb/app/serializers/content_serializers.py index cb6dd4b9c..0f357a07e 100644 --- a/pulp_deb/app/serializers/content_serializers.py +++ b/pulp_deb/app/serializers/content_serializers.py @@ -5,8 +5,15 @@ from debian import deb822, debfile -from rest_framework.serializers import CharField, DictField, Field, ValidationError, Serializer -from pulpcore.plugin.models import Artifact, CreatedResource, RemoteArtifact +from rest_framework.serializers import ( + CharField, + DictField, + ListField, + Field, + ValidationError, + Serializer, +) +from pulpcore.plugin.models import Artifact, Content, CreatedResource, RemoteArtifact from pulpcore.plugin.serializers import ( ContentChecksumSerializer, MultipleArtifactContentSerializer, @@ -721,6 +728,12 @@ class ReleaseSerializer(NoArtifactContentSerializer): A Serializer for Release. """ + def get_unique_together_validators(self): + """ + We do not want UniqueTogetherValidator since we have retrieve logic! + """ + return [] + codename = CharField() suite = CharField() distribution = CharField() @@ -728,6 +741,59 @@ class ReleaseSerializer(NoArtifactContentSerializer): origin = NullableCharField(required=False, allow_null=True, default=None) label = NullableCharField(required=False, allow_null=True, default=None) description = NullableCharField(required=False, allow_null=True, default=None) + architectures = ListField(child=CharField(), required=False) + components = ListField(child=CharField(), required=False) + + @staticmethod + def _get_or_create_content_pk(model, **data): + content, created = model.objects.get_or_create(**data) + CreatedResource(content_object=content).save() + if not created: + content.touch() + return content.pk + + def retrieve(self, validated_data): + """ + If the Release already exists, retrieve it! + """ + return Release.objects.filter( + codename=validated_data["codename"], + suite=validated_data["suite"], + distribution=validated_data["distribution"], + version=validated_data.get("version", NULL_VALUE), + origin=validated_data.get("origin", NULL_VALUE), + label=validated_data.get("label", NULL_VALUE), + description=validated_data.get("description", NULL_VALUE), + ).first() + + def create(self, validated_data): + architectures = validated_data.pop("architectures", []) + components = validated_data.pop("components", []) + repository = validated_data.pop("repository", None) + + content_pks = [] + + release = super().create(validated_data) + content_pks.append(release.pk) + + for arch in architectures: + architecture_pk = self._get_or_create_content_pk( + ReleaseArchitecture, distribution=release.distribution, architecture=arch + ) + content_pks.append(architecture_pk) + + for comp in components: + component_pk = self._get_or_create_content_pk( + ReleaseComponent, distribution=release.distribution, component=comp + ) + content_pks.append(component_pk) + + if repository: + repository.cast() + with repository.new_version() as new_version: + new_version.add_content(Content.objects.filter(pk__in=content_pks)) + + return release class Meta(NoArtifactContentSerializer.Meta): model = Release @@ -739,6 +805,8 @@ class Meta(NoArtifactContentSerializer.Meta): "origin", "label", "description", + "architectures", + "components", ) diff --git a/pulp_deb/app/viewsets/content.py b/pulp_deb/app/viewsets/content.py index 8b001b1e5..267d3a1f6 100644 --- a/pulp_deb/app/viewsets/content.py +++ b/pulp_deb/app/viewsets/content.py @@ -409,7 +409,7 @@ class Meta: fields = ["codename", "suite", "distribution", "version", "label", "origin"] -class ReleaseViewSet(ContentViewSet): +class ReleaseViewSet(NoArtifactContentViewSet): # The doc string is a top level element of the user facing REST API documentation: """ The Release contains release file fields, that are not relevant to the APT repo structure. diff --git a/pulp_deb/tests/functional/api/test_crud_packages.py b/pulp_deb/tests/functional/api/test_crud_packages.py index 0cec84795..4585996e2 100644 --- a/pulp_deb/tests/functional/api/test_crud_packages.py +++ b/pulp_deb/tests/functional/api/test_crud_packages.py @@ -161,3 +161,61 @@ def test_release_architecture_upload( assert repository.latest_version_href.endswith("/1/") assert architecture.pulp_href == architecture2.pulp_href + + +def test_release_comp_arch_upload( + deb_get_repository_by_href, + deb_repository_factory, + deb_release_factory, + apt_release_api, + apt_release_component_api, + apt_release_architecture_api, +): + """Test creating a Release with Components and Architectures directly in a repository.""" + repository = deb_repository_factory() + assert repository.latest_version_href.endswith("/0/") + + distribution = str(uuid4()) + architectures = [str(uuid4()), str(uuid4())] + components = [str(uuid4()), str(uuid4())] + + release_attr = { + "repository": repository.pulp_href, + "distribution": distribution, + "codename": distribution, + "suite": distribution, + "architectures": architectures, + "components": components, + } + + release = deb_release_factory(**release_attr) + repository = deb_get_repository_by_href(repository.pulp_href) + assert repository.latest_version_href.endswith("/1/") + + repo_version_releases = apt_release_api.list(repository_version=repository.latest_version_href) + assert len(repo_version_releases.results) == 1 + assert repo_version_releases.results[0].pulp_href == release.pulp_href + + repo_version_components = apt_release_component_api.list( + repository_version=repository.latest_version_href + ) + + assert len(repo_version_components.results) == 2 + for component in repo_version_components.results: + assert component.distribution == distribution + assert component.component in components + + repo_version_architectures = apt_release_architecture_api.list( + repository_version=repository.latest_version_href + ) + + assert len(repo_version_architectures.results) == 2 + for architecture in repo_version_architectures.results: + assert architecture.distribution == distribution + assert architecture.architecture in architectures + + release2 = deb_release_factory(**release_attr) + repository = deb_get_repository_by_href(repository.pulp_href) + + assert repository.latest_version_href.endswith("/1/") + assert release.pulp_href == release2.pulp_href diff --git a/pulp_deb/tests/functional/conftest.py b/pulp_deb/tests/functional/conftest.py index dd0217ed1..5639e18d5 100644 --- a/pulp_deb/tests/functional/conftest.py +++ b/pulp_deb/tests/functional/conftest.py @@ -21,6 +21,7 @@ Copy, DebAptPublication, DebCopyApi, + DebRelease, DebReleaseArchitecture, DebReleaseComponent, DebSourcePackageReleaseComponent, @@ -163,6 +164,23 @@ def _deb_source_release_component_factory(source_package, release_component, **k return _deb_source_release_component_factory +@pytest.fixture(scope="class") +def deb_release_factory(apt_release_api, gen_object_with_cleanup): + """Fixture that generates deb release with cleanup.""" + + def _deb_release_factory(codename, suite, distribution, **kwargs): + """Create a deb release. + + :returns: The created release. + """ + release_object = DebRelease( + codename=codename, suite=suite, distribution=distribution, **kwargs + ) + return gen_object_with_cleanup(apt_release_api, release_object) + + return _deb_release_factory + + @pytest.fixture(scope="class") def deb_release_component_factory(apt_release_component_api, gen_object_with_cleanup): """Fixture that generates deb package with cleanup."""