Repository: pulp/pulpcore

Issue:
Version Matching for Import/Export too stringent
**Is your feature request related to a problem? Please describe.**
When importing a previously exported content pulp runs a check on pulpcore,  pulp_rpm and other plugin versions of both the export archive and currently installed system. For example if my exporter is using pulp 3.16.1 and import server 3.16.2 I 'd get an error like
`Export version 3.16.1 of pulpcore does not match installed version 3.16.2`

While checking compatibility is a good thing, we need to be mindful of the fact customers in disconnected environments will often have different minor releases of pulp. Expecting the z-stream to also match will make it unwieldy and very hard for us to control the environment.

Location of the check -> https://github.com/pulp/pulpcore/blob/main/pulpcore/app/tasks/importer.py#L93-L118

As described in https://hackmd.io/HLptudH9R6S4PCmm8nRmmg?view#Open-Questions

-   [DONE] How will backwards incompatible changes in the exported data format be handled over time? e.g. I exported this data a loooong time ago and now the system I'm importing into expects a newer, different format?
    -   idea 1: Tie the data format to the exported system's pulpcore version number. Put that version number into the exported data somehow. Then have systems importing know the oldest version they support and refuse to import older exports.
    -   idea 2: Same idea as (1) except use it's own numbering scheme, probably semver based
    -   **Current Status**: versions.json included in export, with core and plugin versions for all
        plugins involved in the export. Import checks for **exact match** to versions.json on import,
        and errors if there are diffs.

We definitely need more flexibility on this.

**Describe the solution you'd like**
Discuss various approaches to solve this in both short  term and long term.

- Only require match of x.y releases and not x.y.z when checking compatibility (not sure that is sufficient).
-  As described in the design docs use a unique numbering/version  scheme that changes only when 2 versions are incompatible. That way if there are no breaking changes between 3.16-3.18 the version will be identical and hence compatible

**Additional context**
Further discussion pending on this topic. Pulpcore needs to be careful to not make it too stringent,


Discussion:
https://bugzilla.redhat.com/show_bug.cgi?id=2067301

Resolving patch:
diff --git a/pulpcore/app/tasks/importer.py b/pulpcore/app/tasks/importer.py
--- a/pulpcore/app/tasks/importer.py
+++ b/pulpcore/app/tasks/importer.py
@@ -92,7 +92,12 @@ def _import_file(fpath, resource_class, retry=False):
 
 
 def _check_versions(version_json):
-    """Compare the export version_json to the installed components."""
+    """
+    Compare the export version_json to the installed components.
+
+    An upstream whose db-metadata doesn't match the downstream won't import successfully; check
+    for compatibility and raise a ValidationError if incompatible versions are found.
+    """
     error_messages = []
     for component in version_json:
         try:
@@ -102,10 +107,13 @@ def _check_versions(version_json):
                 _("Export uses {} which is not installed.").format(component["component"])
             )
         else:
-            if version != component["version"]:
+            # Check that versions are compatible. Currently, "compatible" is defined as "same X.Y".
+            # Versions are strings that generally look like "X.Y.Z" or "X.Y.Z.dev"; we check that
+            # first two places are the same.
+            if version.split(".")[:2] != component["version"].split(".")[:2]:
                 error_messages.append(
                     _(
-                        "Export version {export_ver} of {component} does not match "
+                        "Export version {export_ver} of {component} incompatible with "
                         "installed version {ver}."
                     ).format(
                         export_ver=component["version"],
@@ -114,8 +122,8 @@ def _check_versions(version_json):
                     )
                 )
 
-        if error_messages:
-            raise ValidationError((" ".join(error_messages)))
+    if error_messages:
+        raise ValidationError((" ".join(error_messages)))
 
 
 def import_repository_version(importer_pk, destination_repo_pk, source_repo_name, tar_path):


Test patch:
diff --git a/pulpcore/tests/unit/test_import_checks.py b/pulpcore/tests/unit/test_import_checks.py
new file mode 100644
--- /dev/null
+++ b/pulpcore/tests/unit/test_import_checks.py
@@ -0,0 +1,28 @@
+from unittest.mock import patch
+
+from django.test import TestCase
+from rest_framework.serializers import ValidationError
+
+from pulpcore.app.tasks.importer import _check_versions
+
+
+class TestObject:
+    version = "1.2.3"  # Every component is vers 1.2.3
+
+
+class TestCheckVersions(TestCase):
+    @patch("pulpcore.app.tasks.importer.get_distribution", return_value=TestObject())
+    def test_vers_check(self, mock_get_distribution):
+        export_json = [{"component": "xyz", "version": "1.2.3"}]
+        _check_versions(export_json)
+
+        export_json = [{"component": "xy", "version": "1.2"}]
+        _check_versions(export_json)
+
+        export_json = [{"component": "x_noty_z", "version": "1.4.3"}]
+        with self.assertRaises(ValidationError):
+            _check_versions(export_json)
+
+        export_json = [{"component": "notx_y_z", "version": "2.2.3"}]
+        with self.assertRaises(ValidationError):
+            _check_versions(export_json)