def validate_entry_point(self, config_module, path_to_plugin): """Validates a plugin's entry point. Returns True or throws a PluginValidationError An entry point is considered valid if the config has an entry with key PLUGIN_ENTRY and the value is a path to either a file or the name of a runnable Python module. :param config_module: The previously loaded configuration for the plugin :param path_to_plugin: The path to the root of the plugin """ if config_module is None: raise PluginValidationError( "Configuration is None. This is not allowed.") if path_to_plugin is None: raise PluginValidationError( "Path to Plugin is None. This is not allowed.") if not hasattr(config_module, self.ENTRY_POINT_KEY): raise PluginValidationError( "No %s defined in the plugin configuration." % self.ENTRY_POINT_KEY) entry_point = getattr(config_module, self.ENTRY_POINT_KEY) if isfile(join(path_to_plugin, entry_point)): return elif entry_point.startswith("-m "): pkg_path = join(path_to_plugin, entry_point[3:]) if (isdir(pkg_path) and isfile(join(pkg_path, "__init__.py")) and isfile(join(pkg_path, "__main__.py"))): return else: raise PluginValidationError( "The %s must be a Python script or a runnable Python package: %s" % (self.ENTRY_POINT_KEY, entry_point))
def validate_plugin_config(self, path_to_plugin): """Validates that there is a beer.conf file in the path_to_plugin :param path_to_plugin: Path to the plugin :return: True if beer.conf exists :raises: PluginValidationError if beer.conf is not found """ if path_to_plugin is None: raise PluginValidationError("Attempted to validate plugin config, " "but the plugin_path is None.") path_to_config = join(path_to_plugin, self.CONFIG_NAME) if not isfile(path_to_config): raise PluginValidationError( "Could not validate config file. It does not exist.") config_module = self._load_plugin_config(path_to_plugin) self.validate_required_config_keys(config_module) self.logger.debug("Required keys are present.") self.validate_entry_point(config_module, path_to_plugin) self.logger.debug("Validated Plugin Entry Point successfully.") self.validate_instances_and_args(config_module) self.logger.debug( "Validated plugin instances & arguments successfully.") self.validate_plugin_environment(config_module) self.logger.debug("Validated Plugin Environment successfully.") return True
def validate_plugin_environment(self, config_module): """Validates ENVIRONMENT if specified. ENVIRONMENT must be a dictionary of Strings to Strings. Otherwise it is invalid. :param config_module: :return: True if valid :raises: PluginValidationError if something goes wrong while validating """ if config_module is None: self.logger.error("Configuration is None. This is not allowed.") raise PluginValidationError( "Configuration is None. This is not allowed.") if hasattr(config_module, "ENVIRONMENT"): env = config_module.ENVIRONMENT if not isinstance(env, dict): self.logger.error( "Invalid ENVIRONMENT specified: %s. This argument must " "be a dictionary.", env, ) raise PluginValidationError( "Invalid ENVIRONMENT specified: %s. This argument " "must be a dictionary." % env) for key, value in env.items(): if not isinstance(key, str): self.logger.error( "Invalid Key: %s specified for plugin environment. " "This must be a String.", key, ) raise PluginValidationError( "Invalid Key: %s specified for plugin " "environment. This must be a String." % key) if key.startswith("BG_"): self.logger.error( "Invalid key: %s specified for plugin environment. The 'BG_' prefix is a " "special case for beer-garden only environment variables. You will have to " "pick another name. Sorry for the inconvenience." % key) raise PluginValidationError( "Invalid key: %s specified for plugin environment. The 'BG_' prefix " "is a special case for beer-garden only environment variables. You will " "have to pick another name. Sorry for the inconvenience." % key) if not isinstance(value, str): self.logger.error( "Invalid Key: %s specified for plugin environment. This must be a String.", value, ) raise PluginValidationError( "Invalid Value: %s specified for plugin environment. This must be a " "String." % value) return True
def validate_required_config_keys(self, config_module): if config_module is None: raise PluginValidationError( "Configuration is None. This is not allowed.") for key in self.REQUIRED_KEYS: if not hasattr(config_module, key): raise PluginValidationError( "Required key '%s' is not present. " "This is not allowed." % key)
def validate_plugin_path(self, path_to_plugin): """Validates that a plugin path is actually a path and not a single file.""" if path_to_plugin is None or not isdir(path_to_plugin): raise PluginValidationError('Plugin path "%s" is not a directory' % path_to_plugin) return True
def validate_instances_and_args(self, config_module): if config_module is None: raise PluginValidationError( "Configuration is None. This is not allowed.") plugin_args = getattr(config_module, self.ARGS_KEY, None) instances = getattr(config_module, self.INSTANCES_KEY, None) if instances is not None and not isinstance(instances, list): raise PluginValidationError( "'%s' entry was not None or a list. This is invalid. " "Got: %s" % (self.INSTANCES_KEY, instances)) if plugin_args is None: return True elif isinstance(plugin_args, list): return self.validate_individual_plugin_arguments(plugin_args) elif isinstance(plugin_args, dict): for instance_name, instance_args in plugin_args.items(): if instances is not None and instance_name not in instances: raise PluginValidationError( "'%s' contains key '%s' but that instance is not specified in the '%s'" "entry." % (self.ARGS_KEY, instance_name, self.INSTANCES_KEY)) self.validate_individual_plugin_arguments(instance_args) if instances: for instance_name in instances: if instance_name not in plugin_args.keys(): raise PluginValidationError( "'%s' contains key '%s' but that instance is not specified in the " "'%s' entry." % (self.INSTANCES_KEY, instance_name, self.ARGS_KEY)) return True else: raise PluginValidationError( "'%s' entry was not a list or dictionary. This is invalid. " "Got: %s" % (self.ARGS_KEY, plugin_args))
def validate_individual_plugin_arguments(self, plugin_args): """Validates an individual PLUGIN_ARGS entry""" if plugin_args is not None and not isinstance(plugin_args, list): self.logger.error( "Invalid Plugin Argument Specified. It was not a list or None. " "This is not allowed") raise PluginValidationError( "Invalid Plugin Argument Specified: %s. It was not a " "list or None. This is not allowed." % plugin_args) if isinstance(plugin_args, list): for plugin_arg in plugin_args: if not isinstance(plugin_arg, str): self.logger.error( "Invalid plugin argument: %s - this argument must be a string", plugin_arg, ) raise PluginValidationError( "Invalid plugin argument: %s - this argument must be a string." % plugin_arg) return True
class LocalPluginValidatorTest(unittest.TestCase): def setUp(self): self.validator = LocalPluginValidator() @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_path", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_config", Mock(), ) def test_validate_plugin_calls(self): rv = self.validator.validate_plugin("/path/to/plugin") self.assertEqual(self.validator.validate_plugin_path.call_count, 1) self.assertEqual(self.validator.validate_plugin_config.call_count, 1) self.assertEqual(rv, True) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_path", Mock(side_effect=PluginValidationError("foo")), ) def test_validate_plugin_error(self): rv = self.validator.validate_plugin("/path/to/plugin") self.assertEqual(rv, False) @patch("bartender.local_plugins.validator.isfile", Mock(return_value=True)) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_path", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_entry_point", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_instances_and_args", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_environment", Mock(), ) @patch("bartender.local_plugins.validator.load_source") def test_validate_plugin_load_plugin_config(self, mock_load): mock_load.return_value = {} self.validator.validate_plugin("/path/to/plugin") mock_load.assert_called_with( "BGPLUGINCONFIG", "/path/to/plugin/%s" % self.validator.CONFIG_NAME ) @patch("bartender.local_plugins.validator.isfile", Mock(return_value=True)) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_path", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_entry_point", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_instances_and_args", Mock(), ) @patch( "bartender.local_plugins.validator.LocalPluginValidator.validate_plugin_environment", Mock(), ) @patch("bartender.local_plugins.validator.load_source") def test_validate_remove_plugin_config_from_sys_modules(self, mock_load): def side_effect(module_name, value): sys.modules[module_name] = value mock_load.side_effect = side_effect self.validator.validate_plugin("/path/to/plugin") self.assertNotIn("BGPLUGINCONFIG", sys.modules) def test_validate_plugin_path_bad(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_path, "/path/to/non-existant/foo", ) def test_validate_plugin_path_none(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_path, None ) def test_validate_plugin_path_good(self): current_directory = os.path.dirname(os.path.abspath(__file__)) self.assertTrue(self.validator.validate_plugin_path(current_directory)) def test_validate_plugin_config_none(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_config, None ) @patch("bartender.local_plugins.validator.isfile") def test_validate_plugin_config_not_a_file(self, isfile_mock): isfile_mock.return_value = False self.assertRaises( PluginValidationError, self.validator.validate_plugin_config, "not_a_file" ) isfile_mock.assert_called_with("not_a_file/%s" % self.validator.CONFIG_NAME) def test_validate_entry_point_none_config_module(self): self.assertRaises( PluginValidationError, self.validator.validate_entry_point, None, "/path/to/plugin", ) def test_validate_entry_point_none_path_to_plugin(self): self.assertRaises( PluginValidationError, self.validator.validate_entry_point, {}, None ) def test_validate_entry_point_no_entry_point_key(self): self.assertRaises( PluginValidationError, self.validator.validate_entry_point, Mock(spec=[]), "/path/to/plugin", ) def test_validate_entry_point_bad_entry_point(self): self.assertRaises( PluginValidationError, self.validator.validate_entry_point, Mock(spec=[self.validator.ENTRY_POINT_KEY], PLUGIN_ENTRY="not_a_file"), "/path/to/plugin", ) @patch("bartender.local_plugins.validator.isfile", Mock(return_value=True)) def test_validate_entry_point_good_file(self): self.validator.validate_entry_point( Mock(PLUGIN_ENTRY="is_totally_a_file"), "/path/to/plugin" ) @patch("bartender.local_plugins.validator.isdir", Mock(return_value=True)) @patch("bartender.local_plugins.validator.isfile") def test_validate_entry_point_good_package(self, isfile_mock): def is_special_file(name): return "__init__" in name or "__main__" in name isfile_mock.side_effect = is_special_file self.validator.validate_entry_point( Mock(PLUGIN_ENTRY="-m plugin"), "/path/to/plugin" ) def test_validate_individual_plugin_arguments_not_none_or_list(self): self.assertRaises( PluginValidationError, self.validator.validate_individual_plugin_arguments, "notalistornone", ) def test_validate_individual_plugin_arguments_none(self): self.assertTrue(self.validator.validate_individual_plugin_arguments(None)) def test_validate_individual_plugin_arguments_bad_list(self): self.assertRaises( PluginValidationError, self.validator.validate_individual_plugin_arguments, [{"foo": "bar"}], ) def test_validate_individual_plugin_arguments_good_list(self): self.assertTrue(self.validator.validate_individual_plugin_arguments(["good"])) def test_validate_plugin_environment_none_config(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_environment, None ) def test_validate_plugin_environment_no_environment(self): self.assertTrue(self.validator.validate_plugin_environment(Mock(spec=[]))) def test_validate_plugin_environment_bad_environment(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_environment, Mock(ENVIRONMENT="notadict"), ) def test_validate_plugin_environment_bad_key(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_environment, Mock(ENVIRONMENT={1: "int_key_not_allowed"}), ) def test_validate_plugin_environment_with_bg_prefix_key(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_environment, Mock(ENVIRONMENT={"BG_foo": "that_key_is_not_allowed"}), ) def test_validate_plugin_environment_bad_value(self): self.assertRaises( PluginValidationError, self.validator.validate_plugin_environment, Mock(ENVIRONMENT={"foos": ["foo1", "foo2"]}), ) def test_validate_plugin_environment_good(self): self.assertTrue( self.validator.validate_plugin_environment(Mock(ENVIRONMENT={"foo": "bar"})) ) @patch("bartender.local_plugins.validator.isfile", Mock(return_value=True)) @patch("bartender.local_plugins.validator.LocalPluginValidator._load_plugin_config") def test_validate_plugin_config_missing_required_key(self, load_config_mock): config_module = Mock( VERSION="0.0.1", PLUGIN_ENTRY="/path/to/entry.py", spec=["VERSION", "PLUGIN_ENTRY"], ) load_config_mock.return_value = config_module self.assertRaises( PluginValidationError, self.validator.validate_plugin_config, "/path/to/beer.conf", ) @patch("bartender.local_plugins.validator.isfile", Mock(return_value=True)) @patch("bartender.local_plugins.validator.LocalPluginValidator._load_plugin_config") def test_validate_plugin_config_good(self, load_config_mock): config_module = Mock( NAME="name", VERSION="0.0.1", PLUGIN_ENTRY="/path/to/entry.py", spec=["NAME", "VERSION", "PLUGIN_ENTRY"], ) load_config_mock.return_value = config_module self.assertTrue(self.validator.validate_plugin_config("/path/to/beer.conf")) def test_validate_instances_and_args_none_config_module(self): self.assertRaises( PluginValidationError, self.validator.validate_instances_and_args, None ) def test_validate_instances_and_args_both_none(self): config_module = Mock(spec=[]) self.assertTrue( PluginValidationError, self.validator.validate_instances_and_args(config_module), ) def test_validate_instances_and_args_invalid_instances(self): config_module = Mock(INSTANCES="THIS_IS_WRONG", PLUGIN_ARGS=None) self.assertRaises( PluginValidationError, self.validator.validate_instances_and_args, config_module, ) def test_validate_instances_and_args_good_args(self): config_module = Mock(INSTANCES=None, PLUGIN_ARGS=["foo", "bar"]) self.assertTrue(self.validator.validate_instances_and_args(config_module)) def test_validate_instances_and_args_invalid_args(self): config_module = Mock(INSTANCES=None, PLUGIN_ARGS="THIS IS WRONG") self.assertRaises( PluginValidationError, self.validator.validate_instances_and_args, config_module, ) def test_validate_instances_and_args_invalid_plugin_arg_key(self): config_module = Mock(INSTANCES=["foo"], PLUGIN_ARGS={"bar": ["arg1"]}) self.assertRaises( PluginValidationError, self.validator.validate_instances_and_args, config_module, ) def test_validate_instances_and_args_missing_plugin_arg_key(self): config_module = Mock(INSTANCES=["foo"], PLUGIN_ARGS={}) self.assertRaises( PluginValidationError, self.validator.validate_instances_and_args, config_module, ) def test_validate_instance_and_args_both_provided_good(self): config_module = Mock(INSTANCES=["foo"], PLUGIN_ARGS={"foo": ["arg1"]}) self.assertTrue(self.validator.validate_instances_and_args(config_module))