Repository: pulp/pulpcore

Issue:
PulpImport/Export of kickstart repos with subrepos broken
See https://bugzilla.redhat.com/show_bug.cgi?id=2040870 for details.


Discussion:
The import/export engine in pulpcore needs to expose a hook to plugins to let them describe contained/dependent/sub repos. Content, Artifact, and ContentArtifact processing currently make assumptions that are incomplete in the presence of subrepos, and our testing was insufficient to uncover the problem.

Resolving patch:
diff --git a/pulpcore/app/importexport.py b/pulpcore/app/importexport.py
--- a/pulpcore/app/importexport.py
+++ b/pulpcore/app/importexport.py
@@ -143,10 +143,6 @@ def _combine_content_mappings(map1, map2):
         )
     )
 
-    # Export the connection between content and artifacts
-    resource = ContentArtifactResource(repository_version)
-    _write_export(export.tarfile, resource, dest_dir)
-
     # content mapping is used by repo versions with subrepos (eg distribution tree repos)
     content_mapping = {}
 
@@ -163,6 +159,10 @@ def _combine_content_mappings(map1, map2):
                     content_mapping, resource.content_mapping
                 )
 
+    # Export the connection between content and artifacts
+    resource = ContentArtifactResource(repository_version, content_mapping)
+    _write_export(export.tarfile, resource, dest_dir)
+
     msg = (
         f"Exporting content for {plugin_name} "
         f"repository-version {repository_version.repository.name}/{repository_version.number}"
diff --git a/pulpcore/app/modelresource.py b/pulpcore/app/modelresource.py
--- a/pulpcore/app/modelresource.py
+++ b/pulpcore/app/modelresource.py
@@ -66,12 +66,19 @@ class ContentArtifactResource(QueryModelResource):
     ContentArtifact is different from other import-export entities because it has no 'natural key'
     other than a pulp_id, which aren't shared across instances. We do some magic to link up
     ContentArtifacts to their matching (already-imported) Content.
+
+    Some plugin-models have sub-repositories. We take advantage of the content-mapping
+    machinery to account for those contentartifacts as well.
     """
 
     artifact = fields.Field(
         column_name="artifact", attribute="artifact", widget=ForeignKeyWidget(Artifact, "sha256")
     )
 
+    def __init__(self, repo_version=None, content_mapping=None):
+        self.content_mapping = content_mapping
+        super().__init__(repo_version)
+
     def before_import_row(self, row, **kwargs):
         """
         Fixes the content-ptr of an incoming content-artifact row at import time.
@@ -92,9 +99,15 @@ def before_import_row(self, row, **kwargs):
         row["content"] = str(linked_content.pulp_id)
 
     def set_up_queryset(self):
-        return ContentArtifact.objects.filter(content__in=self.repo_version.content).order_by(
-            "content", "relative_path"
-        )
+        vers_content = ContentArtifact.objects.filter(content__in=self.repo_version.content)
+        if self.content_mapping:
+            all_content = []
+            for content_ids in self.content_mapping.values():
+                all_content.extend(content_ids)
+            vers_content = vers_content.union(
+                ContentArtifact.objects.filter(content__in=all_content)
+            )
+        return vers_content.order_by("content", "relative_path")
 
     class Meta:
         model = ContentArtifact


Test patch: