From 51ff96080847a5ba63a42259ee3ed94c389bc21f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 29 Aug 2024 21:32:46 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=91=20Suppress=20import=20errors?= =?UTF-8?q?=20in=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are coming from `awx` having convoluted and undeclared dependency tree. --- src/awx_plugins/credentials/injectors.py | 8 ++++-- src/awx_plugins/credentials/plugins.py | 31 +++++++++++++++++++++--- src/awx_plugins/inventory/plugins.py | 8 ++++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/awx_plugins/credentials/injectors.py b/src/awx_plugins/credentials/injectors.py index 4f966d0994..cedba1da6d 100644 --- a/src/awx_plugins/credentials/injectors.py +++ b/src/awx_plugins/credentials/injectors.py @@ -2,9 +2,13 @@ import os import stat import tempfile +from contextlib import suppress as _suppress_exception -from awx.main.utils.execution_environments import to_container_path -from django.conf import settings + +with _suppress_exception(ImportError): + # FIXME: stop suppressing once the circular dependency is untangled + from awx.main.utils.execution_environments import to_container_path + from django.conf import settings import yaml diff --git a/src/awx_plugins/credentials/plugins.py b/src/awx_plugins/credentials/plugins.py index 8f9ff044f5..8844fd4514 100644 --- a/src/awx_plugins/credentials/plugins.py +++ b/src/awx_plugins/credentials/plugins.py @@ -1,7 +1,30 @@ -# Django -# AWX -from awx.main.models.credential import ManagedCredentialType -from django.utils.translation import gettext_noop +from __future__ import annotations + + +try: + # Django + # AWX + from awx.main.models.credential import ManagedCredentialType + from django.utils.translation import gettext_noop +except ImportError: + # FIXME: stop suppressing once the circular dependency is untangled + # FIXME: these stubs are temporary + from dataclasses import dataclass + + @dataclass(frozen=True) + class ManagedCredentialType: + """Managed credential type stub.""" + + namespace: str + name: str + kind: str + inputs: dict[str, list[dict[str, bool | str] | str]] + injectors: dict[str, dict[str, str]] = None + managed: bool = False + + def gettext_noop(_text: str) -> str: + """Emulate a Django-imported no-op.""" + return _text ManagedCredentialType( diff --git a/src/awx_plugins/inventory/plugins.py b/src/awx_plugins/inventory/plugins.py index 2839c1c38c..1ec5679c5a 100644 --- a/src/awx_plugins/inventory/plugins.py +++ b/src/awx_plugins/inventory/plugins.py @@ -1,9 +1,13 @@ import os.path import stat import tempfile +from contextlib import suppress as _suppress_exception -from awx.main.utils.execution_environments import to_container_path -from awx.main.utils.licensing import server_product_name + +with _suppress_exception(ImportError): + # FIXME: stop suppressing once the circular dependency is untangled + from awx.main.utils.execution_environments import to_container_path + from awx.main.utils.licensing import server_product_name import yaml From c212f6ef220510b86f1462de9d4970c137d8ec21 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Thu, 29 Aug 2024 21:32:14 +0200 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=A6=20Declare=20inventory=20plugin?= =?UTF-8?q?=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2b103441fb..9537e30810 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,15 @@ dependencies = [ # runtime deps # https://packaging.python.org/en/latest/guide # GUIDANCE: only add things that this project imports directly # GUIDANCE: only set lower version bounds # "awx_plugins.base_interface.api", # keep `__init__.py` empty + "Django", # <- credentials.injectors (and inventory.plugins indirectly) + "PyYAML", # credentials.injectors, inventory.plugins + "azure-identity", # credentials.azure_kv + "azure-keyvault", # credentials.azure_kv + "boto3", # credentials.awx_secretsmanager + "msrestazure", # credentials.azure_kv + "python-dsv-sdk >= 1.0.4", # credentials.thycotic_dsv + "python-tss-sdk >= 1.2.1", # credentials.thycotic_tss + "requests", # credentials.aim, credentials.centrify_vault, credentials.conjur, credentials.hashivault ] classifiers = [ # Allowlist: https://pypi.org/classifiers/ "Development Status :: 1 - Planning", From 60dc36c869a2a3a98fd2d7fc20630f4aca32ba50 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Fri, 16 Aug 2024 14:10:49 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=A6=20Declare=20awx=20entry=20poin?= =?UTF-8?q?ts=20for=20moved=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch includes tests for loading them. --- pyproject.toml | 29 ++++- src/awx_plugins/credentials/x/api.py | 5 - tests/importable_test.py | 184 +++++++++++++++++++++++++-- 3 files changed, 197 insertions(+), 21 deletions(-) delete mode 100644 src/awx_plugins/credentials/x/api.py diff --git a/pyproject.toml b/pyproject.toml index 9537e30810..4ad696ee74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,12 +74,29 @@ name = "Ansible maintainers and contributors" # PLUGIN ACTIVATION GUIDANCE (UX): # `pip install awx_plugins.credentials.x` would auto-activate any plugins the packaged project ships [project.entry-points."awx_plugins.credentials"] # new entry points group name -x = "awx_plugins.credentials.x.api:XPlugin" - -# awx calls `importlib.metadata.entry_points(group='awx.credential_plugins')` to discover and later enable any plugins present in the same env -[project.entry-points."awx.credential_plugins"] # pre-existing entry points group name -x = "awx_plugins.credentials.x.api:XPlugin" -# conjur = awx.main.credential_plugins.conjur:conjur_plugin +conjur = "awx_plugins.credentials.conjur:conjur_plugin" +hashivault_kv = "awx_plugins.credentials.hashivault:hashivault_kv_plugin" +hashivault_ssh = "awx_plugins.credentials.hashivault:hashivault_ssh_plugin" +azure_kv = "awx_plugins.credentials.azure_kv:azure_keyvault_plugin" +aim = "awx_plugins.credentials.aim:aim_plugin" +centrify_vault_kv = "awx_plugins.credentials.centrify_vault:centrify_plugin" +thycotic_dsv = "awx_plugins.credentials.dsv:dsv_plugin" +thycotic_tss = "awx_plugins.credentials.tss:tss_plugin" +aws_secretsmanager_credential = "awx_plugins.credentials.aws_secretsmanager:aws_secretmanager_plugin" + +[project.entry-points."awx_plugins.inventory"] # new entry points group name +azure-rm = "awx_plugins.inventory.plugins:azure_rm" +ec2 = "awx_plugins.inventory.plugins:ec2" +gce = "awx_plugins.inventory.plugins:gce" +vmware = "awx_plugins.inventory.plugins:vmware" +openstack = "awx_plugins.inventory.plugins:openstack" +rhv = "awx_plugins.inventory.plugins:rhv" +satellite6 = "awx_plugins.inventory.plugins:satellite6" +terraform = "awx_plugins.inventory.plugins:terraform" +controller = "awx_plugins.inventory.plugins:controller" +insights = "awx_plugins.inventory.plugins:insights" +openshift_virtualization = "awx_plugins.inventory.plugins:openshift_virtualization" +constructed = "awx_plugins.inventory.plugins:constructed" [project.license] file = "LICENSE" diff --git a/src/awx_plugins/credentials/x/api.py b/src/awx_plugins/credentials/x/api.py deleted file mode 100644 index 8139e413fb..0000000000 --- a/src/awx_plugins/credentials/x/api.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Plugin entry point module.""" - - -class XPlugin: # pylint: disable=too-few-public-methods - """Plugin entry point.""" diff --git a/tests/importable_test.py b/tests/importable_test.py index 9caa9b56c1..d6f172d3c9 100644 --- a/tests/importable_test.py +++ b/tests/importable_test.py @@ -1,26 +1,190 @@ """Smoke tests related to loading entry points.""" +from dataclasses import dataclass from importlib.metadata import entry_points as _discover_entry_points import pytest +from awx_plugins.inventory.plugins import PluginFileInjector -@pytest.mark.parametrize( - 'entry_points_group', - ( - 'awx.credential_plugins', + +@dataclass(frozen=True) +class EntryPointParam: + """Data structure representing a single exposed plugin.""" + + group: str + name: str + spec: str + + def __str__(self): + """Render an entry-point parameter as a string. + + To be used as a part of parametrized test ID. + """ + return f'{self.name}={self.spec}@{self.group}' + + +credential_plugins = ( + EntryPointParam( + 'awx_plugins.credentials', + 'aim', + 'awx_plugins.credentials.aim:aim_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'conjur', + 'awx_plugins.credentials.conjur:conjur_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'hashivault_kv', + 'awx_plugins.credentials.hashivault:hashivault_kv_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'hashivault_ssh', + 'awx_plugins.credentials.hashivault:hashivault_ssh_plugin', + ), + EntryPointParam( 'awx_plugins.credentials', + 'azure_kv', + 'awx_plugins.credentials.azure_kv:azure_keyvault_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'centrify_vault_kv', + 'awx_plugins.credentials.centrify_vault:centrify_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'thycotic_dsv', + 'awx_plugins.credentials.dsv:dsv_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'thycotic_tss', + 'awx_plugins.credentials.tss:tss_plugin', + ), + EntryPointParam( + 'awx_plugins.credentials', + 'aws_secretsmanager_credential', + 'awx_plugins.credentials.aws_secretsmanager:aws_secretmanager_plugin', ), ) -def test_entry_points_exposed(entry_points_group: str) -> None: - """Verify the plugin entry point is discoverable. + + +inventory_plugins = ( + EntryPointParam( + 'awx_plugins.inventory', + 'azure-rm', + 'awx_plugins.inventory.plugins:azure_rm', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'ec2', + 'awx_plugins.inventory.plugins:ec2', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'gce', + 'awx_plugins.inventory.plugins:gce', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'vmware', + 'awx_plugins.inventory.plugins:vmware', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'openstack', + 'awx_plugins.inventory.plugins:openstack', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'rhv', + 'awx_plugins.inventory.plugins:rhv', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'satellite6', + 'awx_plugins.inventory.plugins:satellite6', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'terraform', + 'awx_plugins.inventory.plugins:terraform', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'controller', + 'awx_plugins.inventory.plugins:controller', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'insights', + 'awx_plugins.inventory.plugins:insights', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'openshift_virtualization', + 'awx_plugins.inventory.plugins:openshift_virtualization', + ), + EntryPointParam( + 'awx_plugins.inventory', + 'constructed', + 'awx_plugins.inventory.plugins:constructed', + ), +) + + +with_credential_plugins = pytest.mark.parametrize( + 'entry_point', + credential_plugins, + ids=str, +) + + +with_inventory_plugins = pytest.mark.parametrize( + 'entry_point', + inventory_plugins, + ids=str, +) + + +with_all_plugins = pytest.mark.parametrize( + 'entry_point', + credential_plugins + inventory_plugins, + ids=str, +) + + +@with_all_plugins +def test_entry_points_exposed(entry_point: str) -> None: + """Verify the plugin entry points are discoverable. This check relies on the plugin-declaring distribution package to be pre-installed. """ - entry_points = _discover_entry_points(group=entry_points_group) - assert 'x' in entry_points.names + entry_points = _discover_entry_points(group=entry_point.group) + + assert entry_point.name in entry_points.names + assert entry_points[entry_point.name].value == entry_point.spec + + +@with_credential_plugins +def test_entry_points_are_credential_plugin(entry_point: str) -> None: + """Ensure all exposed credential plugins are of the same class.""" + entry_points = _discover_entry_points(group=entry_point.group) + loaded_plugin_class = entry_points[entry_point.name].load() + + loaded_plugin_class_name = type(loaded_plugin_class).__name__ + assert loaded_plugin_class_name == 'CredentialPlugin' + - assert entry_points['x'].value == 'awx_plugins.credentials.x.api:XPlugin' +@with_inventory_plugins +def test_entry_points_are_inventory_plugin(entry_point: str) -> None: + """Ensure all exposed inventory plugins are of the same class.""" + entry_points = _discover_entry_points(group=entry_point.group) + loaded_plugin_class = entry_points[entry_point.name].load() - assert callable(entry_points['x'].load()) + assert issubclass(loaded_plugin_class, PluginFileInjector)