Repository: hyperledger/aries-cloudagent-python

Issue:
Proposal: Plugin configuration
In our work with pluggable DID resolvers, I found that we needed the ability to configure the plugin at startup to set values relevant to the resolver plugin. However, as a plugin, we can't add command line arguments to the ACA-Py instance so we resorted to loading a separate yaml file specified by an environment variable. This worked fine but it got me thinking about easier to use solutions, especially when considering loading multiple resolvers (i.e. uniresolver + didcomm resolver + ...) and anticipating writing more plugins in the near future that will likewise require their own configuration.

So, to field an idea I had, what do we think of a `--plugin-config plugin.yml` command line argument that just loads the yaml into a map in settings and performs no further validation? Plugins would then subsequently look underneath their own namespace and perform validation if necessary for the options they define. In essence, I view this as analogous to `setup.cfg`/`setup.ini`/`pyproject.toml` tool configurations, allowing various plugins to retrieve configuration from a single source rather than having one configuration file and environment variable each. This also avoids having to do multiple passes of command line argument parsing to allow plugins to define their own. This multi-pass parsing is, of course, doable but doesn't feel as clean to me.


Discussion:
Great idea! I say we do it!
https://hackmd.io/ROUzENdpQ12cz3UB9qk1nA

Resolving patch:
diff --git a/aries_cloudagent/config/argparse.py b/aries_cloudagent/config/argparse.py
--- a/aries_cloudagent/config/argparse.py
+++ b/aries_cloudagent/config/argparse.py
@@ -3,6 +3,7 @@
 import abc
 from os import environ
 
+import yaml
 from configargparse import ArgumentParser, Namespace, YAMLConfigFileParser
 from typing import Type
 
@@ -477,6 +478,20 @@ def add_arguments(self, parser: ArgumentParser):
                 "instances of this parameter can be specified."
             ),
         )
+
+        parser.add_argument(
+            "--plugin-config",
+            dest="plugin_config",
+            type=str,
+            required=False,
+            env_var="ACAPY_PLUGINS_CONFIG",
+            help="Load YAML file path that defines external plugins configuration. "
+            "The plugin should be loaded first by --plugin arg. "
+            "Then the config file must be a key-value mapping. "
+            "The key is the plugin argument and "
+            "the value of the specific configuration for that plugin.",
+        )
+
         parser.add_argument(
             "--storage-type",
             type=str,
@@ -545,6 +560,11 @@ def get_settings(self, args: Namespace) -> dict:
         settings = {}
         if args.external_plugins:
             settings["external_plugins"] = args.external_plugins
+
+        if args.plugin_config:
+            with open(args.plugin_config, "r") as stream:
+                settings["plugin_config"] = yaml.safe_load(stream)
+
         if args.storage_type:
             settings["storage_type"] = args.storage_type
 


Test patch:
diff --git a/aries_cloudagent/config/tests/test_argparse.py b/aries_cloudagent/config/tests/test_argparse.py
--- a/aries_cloudagent/config/tests/test_argparse.py
+++ b/aries_cloudagent/config/tests/test_argparse.py
@@ -3,6 +3,7 @@
 from asynctest import TestCase as AsyncTestCase, mock as async_mock
 
 from .. import argparse
+from ..error import ArgsParseError
 from ..util import BoundedInt, ByteSize
 
 
@@ -173,6 +174,33 @@ async def test_general_settings_file(self):
         assert settings.get("external_plugins") == ["foo"]
         assert settings.get("storage_type") == "bar"
 
+    async def test_plugin_config_file(self):
+        """Test file argument parsing."""
+
+        parser = argparse.create_argument_parser()
+        group = argparse.GeneralGroup()
+        group.add_arguments(parser)
+
+        result = parser.parse_args(
+            [
+                "--endpoint",
+                "localhost",
+                "--plugin-config",
+                "./aries_cloudagent/config/tests/test_plugins_config.yaml",
+            ]
+        )
+
+        assert (
+            result.plugin_config
+            == "./aries_cloudagent/config/tests/test_plugins_config.yaml"
+        )
+
+        settings = group.get_settings(result)
+
+        assert settings.get("plugin_config").get("mock_resolver") == {
+            "methods": ["sov", "btcr"]
+        }
+
     async def test_transport_settings_file(self):
         """Test file argument parsing."""
 
diff --git a/aries_cloudagent/config/tests/test_plugins_config.yaml b/aries_cloudagent/config/tests/test_plugins_config.yaml
new file mode 100644
--- /dev/null
+++ b/aries_cloudagent/config/tests/test_plugins_config.yaml
@@ -0,0 +1,4 @@
+mock_resolver:
+  methods:
+       - "sov"
+       - "btcr"
\ No newline at end of file