Repository: conan-io/conan
Pull request: Required Conan version
The general configuration `required_conan_version` validates the current Conan client version to the required version.

If the current version is out of the range, a warning message
is displayed before executing any command.

Changelog: Feature: Configuration for checking the required Conan client version.
Docs: https://github.com/conan-io/docs/pull/1740

closes #7136

/cc @memsharded 

- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [x] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


Problem statement:
[feature] Min (max) conan version checks from conan.conf
Companies would like to introduce a check to guarantee that everyone is using the same version of conan. That should be an easy check with a config in conan.conf.

Possibilities?:

- A min-version check
- A max-version check
- An exact version check
- A range


The modes could be raise or warn.

Hints from issue discussion:

Commit message: #7136 Remove env vars

Signed-off-by: Uilian Ries <uilianries@gmail.com>

Patch to review:
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index b75a3eb299c..72c21aa74cb 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -20,6 +20,7 @@
 from conans.client.cmd.uploader import CmdUpload
 from conans.client.cmd.user import user_set, users_clean, users_list, token_present
 from conans.client.conanfile.package import run_package_method
+from conans.client.conf.required_version import check_required_conan_version
 from conans.client.graph.graph import RECIPE_EDITABLE
 from conans.client.graph.graph_binaries import GraphBinariesAnalyzer
 from conans.client.graph.graph_manager import GraphManager
@@ -233,6 +234,7 @@ def __init__(self, cache_folder=None, output=None, user_io=None, http_requester=
         # Migration system
         migrator = ClientMigrator(self.cache_folder, Version(client_version), self.out)
         migrator.migrate()
+        check_required_conan_version(self.cache_folder, self.out)
         if not get_env(CONAN_V2_MODE_ENVVAR, False):
             # FIXME Remove in Conan 2.0
             sys.path.append(os.path.join(self.cache_folder, "python"))
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index 97dccd54f0a..9bbd6e40c24 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -177,6 +177,7 @@ def get_default_settings_yml(force_v1=False):
     {% endif %}
 
     # config_install_interval = 1h
+    # required_conan_version = >=1.26
 
     [storage]
     # This is the default path, but you can write your own. It must be an absolute path or a
@@ -709,3 +710,10 @@ def config_install_interval(self):
         except Exception as e:
             raise ConanException("Incorrect definition of general.config_install_interval: %s"
                                  % interval)
+
+    @property
+    def required_conan_version(self):
+        try:
+            return self.get_item("general.required_conan_version")
+        except ConanException:
+            return None
diff --git a/conans/client/conf/required_version.py b/conans/client/conf/required_version.py
new file mode 100644
index 00000000000..53062a7c787
--- /dev/null
+++ b/conans/client/conf/required_version.py
@@ -0,0 +1,28 @@
+from conans.client.cache.cache import ClientCache
+from conans.client.graph.range_resolver import satisfying
+from conans import __version__ as client_version
+from conans.errors import ConanException
+
+
+def check_required_conan_version(cache_folder, out):
+    """ Check if the required Conan version in config file matches to the current Conan version
+
+            When required_conan_version is not configured, it's skipped
+            When required_conan_version is configured, Conan's version must matches the required
+            version
+            When it doesn't match, an ConanException is raised
+
+        :param cache_folder: Conan cache folder
+        :param out: Output stream
+        :return: None
+    """
+    cache = ClientCache(cache_folder, out)
+    required_version = cache.config.required_conan_version
+    if required_version:
+        output = ""
+        result = satisfying([client_version], required_version, output)
+        if not result:
+            raise ConanException("The current Conan version ({}) does not match to the required version ({})."
+                     .format(client_version, required_version))
+        elif result != client_version:
+            raise ConanException(result)
diff --git a/conans/test/functional/configuration/required_version_test.py b/conans/test/functional/configuration/required_version_test.py
new file mode 100644
index 00000000000..62d6565f848
--- /dev/null
+++ b/conans/test/functional/configuration/required_version_test.py
@@ -0,0 +1,48 @@
+import unittest
+import mock
+from conans.test.utils.tools import TestClient
+from conans.errors import ConanException
+
+
+class RequiredVersionTest(unittest.TestCase):
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.26.0")
+    def test_wrong_version(self):
+        required_version = "1.23.0"
+        client = TestClient()
+        client.run("config set general.required_conan_version={}".format(required_version))
+        with self.assertRaises(ConanException) as error:
+            client.run("help")
+        self.assertIn("The current Conan version ({}) "
+                      "does not match to the required version ({})."
+                      .format( "1.26.0", required_version), str(error.exception))
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.22.0")
+    def test_exact_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version={}".format("1.22.0"))
+        client.run("help")
+        self.assertNotIn("ERROR", client.out)
+
+    @mock.patch("conans.client.conf.required_version.client_version", "2.1.0")
+    def test_lesser_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version=<3")
+        client.run("help")
+        self.assertNotIn("ERROR", client.out)
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.0.0")
+    def test_greater_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version=>0.1.0")
+        client.run("help")
+        self.assertNotIn("ERROR", client.out)
+
+    def test_bad_format(self):
+        client = TestClient()
+        required_version = "1.0.0.0-foobar"
+        client.run("config set general.required_conan_version={}".format(required_version))
+        with self.assertRaises(ConanException) as error:
+            client.run("help", assert_error=True)
+        self.assertIn("version range expression '1.0.0.0-foobar' is not valid",
+                      str(error.exception))


Code review comments:
diff_hunk: @@ -0,0 +1,48 @@
+import unittest
+import mock
+from conans.test.utils.tools import TestClient
+from conans.errors import ConanException
+
+
+class RequiredVersionTest(unittest.TestCase):
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.26.0")
+    def test_wrong_version(self):
+        required_version = "1.23.0"
+        client = TestClient()
+        client.run("config set general.required_conan_version={}".format(required_version))
+        with self.assertRaises(ConanException) as error:
+            client.run("help")
+        self.assertIn("The current Conan version ({}) "
+                      "does not match to the required version ({})."
+                      .format( "1.26.0", required_version), str(error.exception))
+
+    @mock.patch("conans.client.conf.required_version.client_version", "1.22.0")
+    def test_exact_version(self):
+        client = TestClient()
+        client.run("config set general.required_conan_version={}".format("1.22.0"))
path: conans/test/functional/configuration/required_version_test.py
text: @user1:
```suggestion
        client.run("config set general.required_conan_version=1.22.0")
```

diff_hunk: @@ -0,0 +1,28 @@
+from conans.client.cache.cache import ClientCache
+from conans.client.graph.range_resolver import satisfying
+from conans import __version__ as client_version
+from conans.errors import ConanException
+
+
+def check_required_conan_version(cache_folder, out):
+    """ Check if the required Conan version in config file matches to the current Conan version
+
+            When required_conan_version is not configured, it's skipped
+            When required_conan_version is configured, Conan's version must matches the required
+            version
+            When it doesn't match, an ConanException is raised
+
+        :param cache_folder: Conan cache folder
+        :param out: Output stream
+        :return: None
+    """
+    cache = ClientCache(cache_folder, out)
+    required_version = cache.config.required_conan_version
+    if required_version:
+        output = ""
+        result = satisfying([client_version], required_version, output)
path: conans/client/conf/required_version.py
text: @user1:
IMO we shouldn't couple this functionality to the way we resolve version ranges. It's unlikely that we are going to change it soon, but there was an issue with the proposal. We can still use the same python library, but here we only need one line:

```python
from semver import satisfies

true/false = satisfies(client_version, required_version)    
   
``` 

Do you think so?

@author:
if it works, I agree that is better! Thanks @user1