← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:rework-zcml-overrides into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:rework-zcml-overrides into launchpad:master.

Commit message:
Avoid writing to code tree for config-specific ZCML overrides

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/428905

Launchpad configs often include some ZCML overrides (for example, `configs/testrunner/yuitest.zcml` hooks up a fixture for JavaScript testing).  These were previously loaded by way of service startup code that generated `zcml/+config-overrides.zcml`, which was then loaded by `zcml/webapp.zcml` and `zcml/ftesting.zcml`.

However, this arrangement required that service startup code be able to write to the code tree.  This didn't work well in the context of Juju charms based on the `ols` layer (which arrange for the code tree to be writable only by root), and it essentially amounts to self-modifying code which seems like dubious design in general.

To avoid this problem, implement a new `lp:includeLaunchpadOverrides` ZCML directive which includes all the `*.zcml` files in the current instance's configuration.  This is equivalent to the old arrangement, but without requiring the service to modify its own code tree at startup.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:rework-zcml-overrides into launchpad:master.
diff --git a/lib/lp/scripts/runlaunchpad.py b/lib/lp/scripts/runlaunchpad.py
index 1336ec3..904fe57 100644
--- a/lib/lp/scripts/runlaunchpad.py
+++ b/lib/lp/scripts/runlaunchpad.py
@@ -341,8 +341,6 @@ def start_launchpad(argv=list(sys.argv), setup=None):
     services, argv = split_out_runlaunchpad_arguments(argv[1:])
     argv = process_config_arguments(argv)
     services = get_services_to_run(services)
-    # Create the ZCML override file based on the instance.
-    config.generate_overrides()
     # Many things rely on a directory called 'logs' existing in the current
     # working directory.
     ensure_directory_exists("logs")
@@ -385,8 +383,6 @@ def start_launchpad(argv=list(sys.argv), setup=None):
 
 def start_librarian():
     """Start the Librarian in the background."""
-    # Create the ZCML override file based on the instance.
-    config.generate_overrides()
     # Create the Librarian storage directory if it doesn't already exist.
     prepare_for_librarian()
     pidfile = pidfile_path("librarian")
diff --git a/lib/lp/scripts/utilities/killtestservices.py b/lib/lp/scripts/utilities/killtestservices.py
index 1fc4281..228881c 100755
--- a/lib/lp/scripts/utilities/killtestservices.py
+++ b/lib/lp/scripts/utilities/killtestservices.py
@@ -19,7 +19,6 @@ def main():
     # Tell lp.services.config to use the testrunner config instance, so that
     # we don't kill the real services.
     config.setInstance("testrunner")
-    config.generate_overrides()
     print("Killing Memcached....", end="")
     kill_by_pidfile(MemcachedLayer.getPidFile())
     print("done.")
diff --git a/lib/lp/scripts/utilities/test.py b/lib/lp/scripts/utilities/test.py
index bb852f0..d997a8a 100755
--- a/lib/lp/scripts/utilities/test.py
+++ b/lib/lp/scripts/utilities/test.py
@@ -71,7 +71,6 @@ def configure_environment():
 
     # Tell lp.services.config to use the testrunner config instance.
     config.setInstance("testrunner")
-    config.generate_overrides()
 
     # Remove this module's directory from path, so that zope.testbrowser
     # can import pystone from test:
diff --git a/lib/lp/services/config/__init__.py b/lib/lp/services/config/__init__.py
index 3924ca1..859a38a 100644
--- a/lib/lp/services/config/__init__.py
+++ b/lib/lp/services/config/__init__.py
@@ -16,7 +16,6 @@ import sys
 from lazr.config import ImplicitTypeSchema
 from lazr.config.interfaces import ConfigErrors
 
-from lp.services.osutils import open_for_writing
 from lp.services.propertycache import cachedproperty, get_property_cache
 
 __all__ = [
@@ -232,27 +231,6 @@ class LaunchpadConfig:
                 text = f.read()
             self._config.push(path, text)
 
-    def generate_overrides(self):
-        """Ensure correct config.zcml overrides will be called.
-
-        Call this method before letting any ZCML processing occur.
-        """
-        loader_file = os.path.join(self.root, "zcml/+config-overrides.zcml")
-        loader = open_for_writing(loader_file, "w")
-
-        print(
-            """
-            <configure xmlns="http://namespaces.zope.org/zope";>
-                <!-- This file automatically generated using
-                     lp.services.config.LaunchpadConfig.generate_overrides.
-                     DO NOT EDIT. -->
-                <include files="%s/*.zcml" />
-                </configure>"""
-            % self.config_dir,
-            file=loader,
-        )
-        loader.close()
-
     def appserver_root_url(self, facet="mainsite", ensureSlash=False):
         """Return the correct app server root url for the given facet."""
         root_url = str(getattr(self.vhost, facet).rooturl)
diff --git a/lib/lp/services/config/tests/test_config_lookup.py b/lib/lp/services/config/tests/test_config_lookup.py
index 4f04561..277883b 100644
--- a/lib/lp/services/config/tests/test_config_lookup.py
+++ b/lib/lp/services/config/tests/test_config_lookup.py
@@ -121,30 +121,3 @@ class TestInstanceConfigDirLookup(ConfigTestCase):
         config_file.close()
 
         self.assertEqual(2323, cfg.launchpad.default_batch_size)
-
-
-class TestGenerateOverrides(ConfigTestCase):
-    """Test the generate_overrides method of LaunchpadConfig."""
-
-    def test_generate_overrides(self):
-        instance_dir = self.setUpInstanceConfig("zcmltest")
-        cfg = config.LaunchpadConfig("zcmltest")
-        # The ZCML override file is generated in the root of the tree.
-        # Set that root to the temporary directory.
-        cfg.root = self.temp_config_root_dir
-        cfg.generate_overrides()
-        override_file = os.path.join(cfg.root, "zcml/+config-overrides.zcml")
-        self.assertTrue(
-            os.path.isfile(override_file), "Overrides file wasn't created."
-        )
-
-        fh = open(override_file)
-        overrides = fh.read()
-        fh.close()
-
-        magic_line = '<include files="%s/*.zcml" />' % instance_dir
-        self.assertTrue(
-            magic_line in overrides,
-            "Overrides doesn't contain the magic include line (%s):\n%s"
-            % (magic_line, overrides),
-        )
diff --git a/lib/lp/services/config/tests/test_zcml.py b/lib/lp/services/config/tests/test_zcml.py
new file mode 100644
index 0000000..475ee48
--- /dev/null
+++ b/lib/lp/services/config/tests/test_zcml.py
@@ -0,0 +1,54 @@
+# Copyright 2022 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for lp.services.config.zcml."""
+
+import io
+import os.path
+from textwrap import dedent
+
+from zope.configuration.config import ConfigurationMachine
+from zope.configuration.xmlconfig import (
+    processxmlfile,
+    registerCommonDirectives,
+)
+
+from lp.services.config.fixture import ConfigUseFixture
+from lp.services.config.tests.test_config_lookup import ConfigTestCase
+from lp.testing.layers import BaseLayer
+
+
+class TestIncludeLaunchpadOverrides(ConfigTestCase):
+
+    layer = BaseLayer
+
+    def test_includes_overrides(self):
+        instance_dir = self.setUpInstanceConfig("zcmltest")
+        self.useFixture(ConfigUseFixture("zcmltest"))
+        with open(os.path.join(instance_dir, "test.zcml"), "w") as f:
+            f.write(
+                dedent(
+                    """
+                    <configure xmlns="http://namespaces.zope.org/zope";
+                               xmlns:meta="http://namespaces.zope.org/meta";>
+                        <meta:provides feature="testfeature" />
+                    </configure>
+                    """
+                )
+            )
+        context = ConfigurationMachine()
+        self.assertFalse(context.hasFeature("testfeature"))
+        registerCommonDirectives(context)
+        topfile = io.StringIO(
+            dedent(
+                """
+                <configure xmlns="http://namespaces.zope.org/zope";
+                           xmlns:lp="http://namespaces.canonical.com/lp";>
+                    <include package="lp.services.config" file="meta.zcml" />
+                    <lp:includeLaunchpadOverrides />
+                </configure>
+                """
+            )
+        )
+        processxmlfile(topfile, context, testing=True)
+        self.assertTrue(context.hasFeature("testfeature"))
diff --git a/lib/lp/services/config/zcml.py b/lib/lp/services/config/zcml.py
new file mode 100644
index 0000000..6ace438
--- /dev/null
+++ b/lib/lp/services/config/zcml.py
@@ -0,0 +1,19 @@
+# Copyright 2022 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""ZCML directives relating to Launchpad configuration."""
+
+__all__ = [
+    "include_launchpad_overrides",
+]
+
+import os.path
+
+from zope.configuration.xmlconfig import includeOverrides
+
+from lp.services.config import config
+
+
+def include_launchpad_overrides(context):
+    """Include overrides depending on the Launchpad instance name."""
+    includeOverrides(context, files=os.path.join(config.config_dir, "*.zcml"))
diff --git a/utilities/list-pages b/utilities/list-pages
index 0e64904..187f4e7 100755
--- a/utilities/list-pages
+++ b/utilities/list-pages
@@ -53,7 +53,6 @@ from zope.interface import directlyProvides, implementer
 from zope.publisher.interfaces.browser import IDefaultBrowserLayer
 
 import zcml
-from lp.services.config import config
 from lp.services.scripts import execute_zcml_for_scripts
 from lp.services.webapp.interfaces import ICanonicalUrlData
 from lp.services.webapp.publisher import canonical_url
@@ -64,8 +63,6 @@ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
 
 def load_zcml(zopeless=False):
     """Load ZCML the way we do in the web site."""
-    # We can't load ZCML without generating configuration.
-    config.generate_overrides()
     if zopeless:
         execute_zcml_for_scripts()
     else:
diff --git a/zcml/ftesting.zcml b/zcml/ftesting.zcml
index 9c684eb..35124c0 100644
--- a/zcml/ftesting.zcml
+++ b/zcml/ftesting.zcml
@@ -4,7 +4,8 @@
 
 <!-- Used by the FunctionalLayer in lp.testing.layers. -->
 
-<configure xmlns="http://namespaces.zope.org/zope";>
+<configure xmlns="http://namespaces.zope.org/zope";
+           xmlns:lp="http://namespaces.canonical.com/lp";>
 
     <include file="common.zcml" />
 
@@ -13,7 +14,9 @@
 
     <includeOverrides files="override-includes/*-configure.zcml" />
     <includeOverrides files="override-includes/*-configure-testing.zcml" />
-    <includeOverrides file="+config-overrides.zcml" />
+
+    <include package="lp.services.config" file="meta.zcml" />
+    <lp:includeLaunchpadOverrides />
 
     <include file="summarizerequests.zcml" />
 
diff --git a/zcml/script-testing.zcml b/zcml/script-testing.zcml
index 0d1c6c0..e2ccae8 100644
--- a/zcml/script-testing.zcml
+++ b/zcml/script-testing.zcml
@@ -12,7 +12,7 @@
     <includeOverrides files="override-includes/*-configure.zcml" />
     <includeOverrides files="override-includes/*-configure-testing.zcml" />
 
-    <!-- No +config-overrides here, as the mail config can cause celery
-         tests to fail. -->
+    <!-- No <includeLaunchpadOverrides /> here, as the mail config can cause
+         celery tests to fail. -->
 
 </configure>
diff --git a/zcml/script.zcml b/zcml/script.zcml
index 01ae301..2e7b03e 100644
--- a/zcml/script.zcml
+++ b/zcml/script.zcml
@@ -11,8 +11,8 @@
 
     <includeOverrides files="override-includes/*-configure.zcml" />
     <includeOverrides files="override-includes/*-configure-normal.zcml" />
-    <!-- No +config-overrides here, as the mail config can cause celery
-         tests to fail. -->
+    <!-- No <includeLaunchpadOverrides /> here, as the mail config can cause
+         celery tests to fail. -->
 
     <!-- Add a hook to configure the email stuff using ZCML stored outside
         of the launchpad tree -->
diff --git a/zcml/webapp.zcml b/zcml/webapp.zcml
index f949d9f..929f06c 100644
--- a/zcml/webapp.zcml
+++ b/zcml/webapp.zcml
@@ -2,7 +2,8 @@
      GNU Affero General Public License version 3 (see the file LICENSE).
 -->
 
-<configure xmlns="http://namespaces.zope.org/zope";>
+<configure xmlns="http://namespaces.zope.org/zope";
+           xmlns:lp="http://namespaces.canonical.com/lp";>
 
     <include file="common.zcml" />
 
@@ -11,7 +12,9 @@
 
     <includeOverrides files="override-includes/*-configure.zcml" />
     <includeOverrides files="override-includes/*-configure-normal.zcml" />
-    <includeOverrides file="+config-overrides.zcml" />
+
+    <include package="lp.services.config" file="meta.zcml" />
+    <lp:includeLaunchpadOverrides />
 
     <include file="summarizerequests.zcml" />