URL: https://github.com/freeipa/freeipa/pull/6049
Author: rcritten
 Title: #6049: Make the schema cache TTL user-configurable
Action: opened

PR body:
"""
The API schema is not checked for changes until after a TTL
is expired. A one-hour TTL was hardcoded which makes development
tedious because the only way to force a schema update is to
remember to remove files between invocations.

This adds a new environment variable, schema_ttl, to configure
the TTL returned by the server to schema() calls. This can be
set low to ensure a frequent refresh during development.

If the client is in compat mode, that is if client is working
against a server that doesn't support the schema() command,
then use the client's schema_ttl instead so that the user still
has control.

https://pagure.io/freeipa/issue/8492

Signed-off-by: Rob Crittenden <rcrit...@redhat.com>
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/6049/head:pr6049
git checkout pr6049
From fa4e6c8bb17d935dcddc02d4d4170a017c15962a Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Thu, 14 Oct 2021 17:07:32 -0400
Subject: [PATCH] Make the schema cache TTL user-configurable

The API schema is not checked for changes until after a TTL
is expired. A one-hour TTL was hardcoded which makes development
tedious because the only way to force a schema update is to
remember to remove files between invocations.

This adds a new environment variable, schema_ttl, to configure
the TTL returned by the server to schema() calls. This can be
set low to ensure a frequent refresh during development.

If the client is in compat mode, that is if client is working
against a server that doesn't support the schema() command,
then use the client's schema_ttl instead so that the user still
has control.

https://pagure.io/freeipa/issue/8492

Signed-off-by: Rob Crittenden <rcrit...@redhat.com>
---
 client/man/default.conf.5            |  3 ++
 ipaclient/remote_plugins/__init__.py |  4 +-
 ipaclient/remote_plugins/compat.py   |  5 ++-
 ipaclient/remote_plugins/schema.py   |  2 +-
 ipalib/constants.py                  |  3 ++
 ipaserver/plugins/schema.py          |  8 +---
 ipatests/test_cmdline/test_schema.py | 66 ++++++++++++++++++++++++++++
 7 files changed, 79 insertions(+), 12 deletions(-)
 create mode 100644 ipatests/test_cmdline/test_schema.py

diff --git a/client/man/default.conf.5 b/client/man/default.conf.5
index 1fdceb122ed..9b1f76438e4 100644
--- a/client/man/default.conf.5
+++ b/client/man/default.conf.5
@@ -172,6 +172,9 @@ Specifies the Kerberos realm.
 .B replication_wait_timeout <seconds>
 The time to wait for a new entry to be replicated during replica installation. The default value is 300 seconds.
 .TP
+.B schema_ttl <seconds>
+The number of seconds for the ipa tool to cache the IPA API and help schema. Reducing this value during development is helpful so that API changes are seen sooner in the tool. Setting this on a server will define the TTL for all client versions > 4.3.1. Client veresions > 4.3.1 that connect to IPA servers older than 4.3.1 will use the client-side configuration value. The default is 3600 seconds.
+.TP
 .B server <hostname>
 Specifies the IPA Server hostname.
 .TP
diff --git a/ipaclient/remote_plugins/__init__.py b/ipaclient/remote_plugins/__init__.py
index fe75b81b882..acbc2ddf214 100644
--- a/ipaclient/remote_plugins/__init__.py
+++ b/ipaclient/remote_plugins/__init__.py
@@ -87,9 +87,7 @@ def __iter__(self):
     def __len__(self):
         return len(self._dict)
 
-    def update_validity(self, ttl=None):
-        if ttl is None:
-            ttl = 3600
+    def update_validity(self, ttl):
         self['expiration'] = time.time() + ttl
         self['language'] = self._language
         self._write()
diff --git a/ipaclient/remote_plugins/compat.py b/ipaclient/remote_plugins/compat.py
index 2a600fccc89..351669a961c 100644
--- a/ipaclient/remote_plugins/compat.py
+++ b/ipaclient/remote_plugins/compat.py
@@ -58,7 +58,10 @@ def get_package(server_info, client):
             else:
                 server_version = '2.0'
         server_info['version'] = server_version
-        server_info.update_validity()
+
+        # in compat mode we don't get the schema TTL from the server
+        # so use the client context value.
+        server_info.update_validity(client.api.env.schema_ttl)
 
     server_version = APIVersion(server_version)
 
diff --git a/ipaclient/remote_plugins/schema.py b/ipaclient/remote_plugins/schema.py
index 7bf714371f0..0ac3c94f187 100644
--- a/ipaclient/remote_plugins/schema.py
+++ b/ipaclient/remote_plugins/schema.py
@@ -557,7 +557,7 @@ def get_package(server_info, client):
                 schema = Schema(client, e.fingerprint)
         except NotAvailable:
             fingerprint = None
-            ttl = None
+            ttl = 3600  # set a ttl so we don't hammer the remote server
         except SchemaUpToDate as e:
             fingerprint = e.fingerprint
             ttl = e.ttl
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 2aeafac7ab8..ad610e05741 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -182,6 +182,9 @@
     # How long to wait for a certmonger request to finish
     ('certmonger_wait_timeout', 300),
 
+    # Number of seconds before client should check for schema update.
+    ('schema_ttl', 3600),
+
     # Web Application mount points
     ('mount_ipa', '/ipa/'),
 
diff --git a/ipaserver/plugins/schema.py b/ipaserver/plugins/schema.py
index d56cf28e177..4dac82c4f12 100644
--- a/ipaserver/plugins/schema.py
+++ b/ipaserver/plugins/schema.py
@@ -20,12 +20,6 @@
 from ipalib.text import _
 from ipapython.version import API_VERSION
 
-# Schema TTL sent to clients in response to schema call.
-# Number of seconds before client should check for schema update.
-# This should be long enough to not slow down regular work or skripts
-# but also short enough to ensure schema will be retvieved soon after
-# it was updated
-SCHEMA_TTL = 3600  # default: 1 hour
 
 __doc__ = _("""
 API Schema
@@ -855,7 +849,7 @@ def execute(self, *args, **kwargs):
             schema = self._generate_schema(**kwargs)
             self.api._schema[langs] = schema
 
-        schema['ttl'] = SCHEMA_TTL
+        schema['ttl'] = self.api.env.schema_ttl
 
         if schema['fingerprint'] in kwargs.get('known_fingerprints', []):
             raise errors.SchemaUpToDate(
diff --git a/ipatests/test_cmdline/test_schema.py b/ipatests/test_cmdline/test_schema.py
new file mode 100644
index 00000000000..99533e10a1a
--- /dev/null
+++ b/ipatests/test_cmdline/test_schema.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2021  FreeIPA Contributors see COPYING for license
+#
+import pytest
+import time
+
+from ipaclient.remote_plugins import ServerInfo
+
+
+class TestServerInfo(ServerInfo):
+    """Simplified ServerInfo class with hardcoded values"""
+    def __init__(self, fingerprint='deadbeef', hostname='ipa.example.test',
+                 force_check=False, language='en_us',
+                 version='2.0', expiration=None):
+        self._force_check = force_check
+        self._language = language
+        self._dict = {
+            'fingerprint': fingerprint,
+            'expiration': expiration or time.time() + 3600,
+            'language': language,
+            'version': version,
+        }
+
+    def _read(self):
+        """Running on test controller, this is a no-op"""
+
+    def _write(self):
+        """Running on test controller, this is a no-op"""
+
+
+@pytest.mark.tier0
+class TestIPAServerInfo:
+    """Test that ServerInfo detects changes in remote configuration"""
+
+    def test_valid(self):
+        server_info = TestServerInfo()
+        assert server_info.is_valid() is True
+
+    def test_force_check(self):
+        server_info = TestServerInfo(force_check=True)
+        assert server_info.is_valid() is False
+
+    def test_language_change(self):
+        server_info = TestServerInfo()
+        assert server_info.is_valid() is True
+        server_info._language = 'fr'
+        assert server_info.is_valid() is False
+
+    def test_expired(self):
+        server_info = TestServerInfo(expiration=time.time() - 3600)
+        assert server_info.is_valid() is False
+
+    def test_wait_for_expired(self):
+        server_info = TestServerInfo()
+        assert server_info.is_valid() is True
+        # force the expiration date to now + 1, wait, should be expired
+        server_info.update_validity(1)
+        time.sleep(2)
+        assert server_info.is_valid() is False
+
+    def test_update_validity(self):
+        server_info = TestServerInfo()
+        expiration = server_info['expiration']
+        server_info.update_validity(3600)
+        assert server_info['expiration'] > expiration
+        assert server_info.is_valid() is True
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org
Fedora Code of Conduct: 
https://docs.fedoraproject.org/en-US/project/code-of-conduct/
List Guidelines: https://fedoraproject.org/wiki/Mailing_list_guidelines
List Archives: 
https://lists.fedorahosted.org/archives/list/freeipa-devel@lists.fedorahosted.org
Do not reply to spam on the list, report it: 
https://pagure.io/fedora-infrastructure

Reply via email to