services/ui_backend_service/plugins/plugin.py (127 lines of code) (raw):

import os import json import glob import collections import pygit2 from typing import List from services.utils import logging from aiohttp import web CONFIG_FILENAME = 'manifest.json' INSTALLED_PLUGINS_DIR = 'installed' _dirname = os.path.dirname(os.path.realpath(__file__)) installed_plugins_base_dir = os.environ.get("INSTALLED_PLUGINS_BASE_DIR", _dirname) PluginConfig = collections.namedtuple("PluginConfig", "name version entrypoint") class Plugin(object): identifier: str = None repository: str = None ref: str = None path: str = None parameters: dict = {} config: PluginConfig = {} files: List[str] = [] _repo: pygit2.Repository = None def __init__(self, identifier: str, repository: str, ref: str = None, parameters: dict = {}, path: str = None, auth: dict = {}): self.logger = logging.getLogger("Plugin:{}:{}".format(identifier, path)) self.identifier = identifier self.repository = repository self.ref = ref self.parameters = parameters # Base path for plugin folder self.basepath = os.path.join(installed_plugins_base_dir, INSTALLED_PLUGINS_DIR, self.identifier) self.logger.info("self.basepath: {}".format(self.basepath)) # Path to plugin files such as manifest.json. # Differs from root path in case of multi-plugin repositories. self.filepath = os.path.join(self.basepath, path or "") self.credentials = _get_credentials(auth) self.callbacks = pygit2.RemoteCallbacks(credentials=self.credentials) if self.credentials else None def init(self): """ Init plugin by loading manifest.json and listing available files from filesystem. In case of Git repository, clone, fetch changes and checkout to target ref. """ local_repository = pygit2.discover_repository(self.basepath) if local_repository: self._repo = pygit2.Repository(local_repository) self.checkout(self.repository) elif self.repository: self._repo = pygit2.clone_repository( self.repository, self.basepath, bare=False, callbacks=self.callbacks) self.checkout() else: # Target directory is not a Git repository, no need to checkout pass self.files = self._list_files() if not self.files: raise PluginException("Error loading plugin files", "plugin-error-files") self.config = self._load_config() if not self.config: raise PluginException("Error loading plugin config", "plugin-error-config") return self def checkout(self, repository_url: str = None): """Fetch latest changes and checkout repository""" if self._repo: # Update repository url in case it has changed if repository_url: self._repo.remotes.set_url("origin", repository_url) # Fetch latest changes for remote in self._repo.remotes: remote.fetch(callbacks=self.callbacks) # Resolve ref and checkout commit, resolved_refish = self._repo.resolve_refish(self.ref if self.ref else 'origin/master') self._repo.checkout(resolved_refish, strategy=pygit2.GIT_CHECKOUT_FORCE) self.logger.info("Checkout {} at {}".format(resolved_refish.name, commit.short_id)) @property def name(self) -> str: return self.config.get("name", self.identifier) def _load_config(self) -> PluginConfig: try: config = json.loads(self.get_file(CONFIG_FILENAME)) if not all(key in config for key in ('name', 'version', 'entrypoint')): return None return dict(config) except Exception as e: self.logger.info("load_config exception:{}".format(e)) return None def _list_files(self) -> List[str]: files = [] static_files = glob.glob(os.path.join(self.filepath, '**'), recursive=True) for filepath in static_files: filename = os.path.basename(filepath) if not os.path.isdir(filepath): files.append(filename) return files def has_file(self, filename) -> bool: return filename in self.files def get_file(self, filename): """Return file contents""" if not self.has_file(filename): return None try: with open(os.path.join(self.filepath, filename), 'r') as file: return file.read() except Exception as e: self.logger.info("get_file exception for: {}: {}".format(filename, e)) return None def serve(self, filename): """Serve files from plugin repository""" if not self.has_file(filename): return web.Response(status=404, body="File not found") return web.FileResponse(os.path.join(self.filepath, filename)) def __iter__(self): for key in ["identifier", "name", "repository", "ref", "parameters", "config", "files"]: yield key, getattr(self, key) class PluginException(Exception): def __init__(self, msg="Error loading plugin", id="plugin-error-unknown", traceback_str=None): self.message = msg self.id = id self.traceback_str = traceback_str def __str__(self): return self.message def _get_credentials(_auth): if not _auth: return None _agent = _auth.get("agent", False) _public_key = _auth.get("public_key", None) _private_key = _auth.get("private_key", None) _username = _auth.get("user", None) _passphrase = _auth.get("pass", None) if _agent: credentials = pygit2.KeypairFromAgent(_username or "git") elif _public_key and _private_key: if os.path.exists(_public_key) and os.path.exists(_private_key): credentials = pygit2.Keypair(_username or "git", _public_key, _private_key, _passphrase or "") else: credentials = pygit2.KeypairFromMemory(_username or "git", _public_key, _private_key, _passphrase or "") elif _username and _passphrase: credentials = pygit2.UserPass(_username, _passphrase) elif _username: credentials = pygit2.Username(_username) else: credentials = None return credentials