diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/bossman/__init__.py b/bossman/__init__.py new file mode 100644 index 0000000..d0da577 --- /dev/null +++ b/bossman/__init__.py @@ -0,0 +1,57 @@ +from os.path import abspath, commonpath, join + +from bossman.resources import ResourceManager, ResourceStatus +from bossman.abc.resource_type import ResourceTypeABC +from bossman.abc.resource import ResourceABC +from bossman.changes import Change, ChangeSet +from bossman.config import Config, ResourceTypeConfig +from bossman.logging import get_class_logger +from bossman.repo import Repo + + +class Bossman: + def __init__(self, repo_path, config: Config): + self.config = config + self.repo = Repo(repo_path) + self.resource_manager = ResourceManager() + for config in self.config.resource_types: + resource_type = ResourceTypeABC.create(config) + self.resource_manager.register_resource_type(resource_type) + self.logger = get_class_logger(self) + + def get_resources(self, rev: str = "HEAD") -> list: + return self.resource_manager.get_resources(self.repo, rev) + + def get_resource_status(self, resource: ResourceABC) -> ResourceStatus: + resource_type = self.resource_manager.get_resource_type(resource.path) + local_rev = self.repo.get_last_change_rev(resource.paths) + remote_rev = resource_type.get_remote_rev(resource) + missing_changesets = self.get_changesets(remote_rev, local_rev, [resource]) + dirty = resource_type.is_dirty(resource) + return ResourceStatus( + local_rev=local_rev, + remote_rev=remote_rev, + dirty=dirty, + missing_changesets=missing_changesets + ) + + def get_changesets(self, since_rev: str = None, until_rev: str = "HEAD", resources: list = None) -> str: + from collections import defaultdict + resources = resources if resources else self.get_resources() + paths = [resource.path for resource in resources] + commits = self.repo.get_commits(since_rev, until_rev, paths) + changeSets = [] + for commit in commits: + changeSet = ChangeSet(commit) + changeSets.append(changeSet) + resources = dict() + for diff in commit.diffs: + the_path = diff.b_path or diff.a_path + resource = self.resource_manager.get_resource(the_path) + if resource: + changeSet.add_resource_diff(resource, diff) + return changeSets + + def apply_change(self, changeset: ChangeSet, change: Change): + resource_type = self.resource_manager.get_resource_type(change.resource.path) + resource_type.apply(changeset, change) diff --git a/bossman/abc/__init__.py b/bossman/abc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bossman/abc/resource.py b/bossman/abc/resource.py new file mode 100644 index 0000000..f29e01e --- /dev/null +++ b/bossman/abc/resource.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractproperty + +class ResourceABC(ABC): + """ + A {ResourceABC} is just a thing that lives at a path. + """ + def __init__(self, path): + self.path = path + + @abstractproperty + def paths(self): + pass + + def __eq__(self, other): + if isinstance(other, ResourceABC): + return self.path == other.path + return False + + def __hash__(self): + return hash(self.path) + + def __str__(self): + return self.path diff --git a/bossman/abc/resource_type.py b/bossman/abc/resource_type.py new file mode 100644 index 0000000..6ad8846 --- /dev/null +++ b/bossman/abc/resource_type.py @@ -0,0 +1,79 @@ +from abc import ABC, abstractmethod +from bossman.config import ResourceTypeConfig +from bossman.changes import Change, ChangeSet +from bossman.logging import logger +from bossman.abc.resource import ResourceABC +import parse +from os.path import relpath, join + +class ResourceTypeABC(ABC): + @staticmethod + def create(config: ResourceTypeConfig): + import importlib + plugin = importlib.import_module(config.module) + return plugin.ResourceType(config) + + """ + Abstract class for resource_types. + """ + def __init__(self, config: ResourceTypeConfig): + self.config = config + self.logger = logger.getChild( + "{module_name}.{class_name}".format( + module_name=self.__class__.__module__, + class_name=self.__class__.__name__ + ) + ) + self.logger.info("config={config}".format(config=str(config))) + + def get_resources(self, paths: list): + """ + Given a list of paths relative to the repo, determine the subset managed + by this resource type and return the list of {ResourceABC}s. + """ + return set( + resource + for resource + in ( + self.get_resource(path) + for path + in paths + ) + if resource != None + ) + + def match(self, path: str) -> bool: + result = parse.search(self.config.pattern, path) + if result: + canonical = self.config.pattern.format(**result.named) + return path.startswith(canonical) + return False + + def get_resource(self, path: str) -> ResourceABC: + result = parse.search(self.config.pattern, path) + if result: + canonical = self.config.pattern.format(**result.named) + if path.startswith(canonical): + return self.create_resource(canonical, **result.named) + return None + + def describe_diffs(self, resource: ResourceABC, diffs: list) -> Change: + change = Change(resource) + change.diffs = diffs + return change + + @abstractmethod + def create_resource(self, path, **kwargs) -> ResourceABC: + pass + + @abstractmethod + def get_remote_rev(self, resource: ResourceABC) -> str: + pass + + @abstractmethod + def is_dirty(self, resource: ResourceABC) -> bool: + pass + + @abstractmethod + def apply(self, changeset: ChangeSet, change: Change) -> bool: + pass diff --git a/bossman/changes.py b/bossman/changes.py new file mode 100644 index 0000000..9c4fd1d --- /dev/null +++ b/bossman/changes.py @@ -0,0 +1,45 @@ +from os.path import basename +from bossman.repo import Commit, Diff +from bossman.abc.resource import ResourceABC +from collections import OrderedDict + +class ChangeSet: + def __init__(self, commit: Commit): + self.rev = commit.rev + self.message = commit.message + self.author = commit.author + self.date = commit.date + self.resource_changes = OrderedDict() + + def add_resource_diff(self, resource: ResourceABC, diff: Diff): + if not (resource in self.resource_changes): + self.resource_changes[resource] = Change(resource) + change = self.resource_changes.get(resource) + change.diffs.append(diff) + + def __str__(self): + s = "[{rev}] {date} {message} | {author}".format(rev=self.rev, message=self.message.split("\n")[0], author=self.author, date=self.date) + if len(self.resource_changes): + s += "\n\n " + s += "\n ".join(str(change) for change in self.resource_changes.values()) + s += "\n" + return s + +class Change: + def __init__(self, resource: ResourceABC): + self.resource = resource + self.diffs = [] + + def __str__(self): + s = "{resource:<58} (".format(resource=str(self.resource)) + s += ", ".join(map(format_diff, self.diffs)) + s += ")" + return s + +def format_diff(diff): + if diff.change_type in 'D': + return diff.change_type + " " + basename(diff.a_path) + elif diff.change_type == 'R': + return diff.change_type + " " + basename(diff.a_path) + " -> " + basename(diff.b_path) + else: + return diff.change_type + " " + basename(diff.b_path) diff --git a/bossman/cli/__init__.py b/bossman/cli/__init__.py new file mode 100644 index 0000000..df49b27 --- /dev/null +++ b/bossman/cli/__init__.py @@ -0,0 +1,52 @@ +import sys +from os import path, getcwd +import yaml +import argparse +from bossman.cli import status_cmd, log_cmd, apply_cmd +from bossman.config import Config +from bossman.logging import logger +from bossman import Bossman +from bossman.config import Config + +logger = logger.getChild(globals().get("__name__")) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--verbosity", action=SetVerbosity, default="ERROR", type=str, choices=["DEBUG", "INFO", "WARNING", "ERROR"]) + parser.add_argument("--repo", help="path to the repository", default=getcwd()) + + subparsers = parser.add_subparsers(title="Subcommands") + status_cmd.init(subparsers) + log_cmd.init(subparsers) + apply_cmd.init(subparsers) + + args = parser.parse_args() + bossman = create_bossman(args) + + if "func" in args: + args.func(bossman, **vars(args)) + else: + parser.print_usage() + +def create_bossman(args): + conf_path = path.join(args.repo, ".bossman") + if path.exists(conf_path): + fd = open(conf_path, "r") + conf_data = yaml.safe_load(fd) + + config = Config(conf_data) + + bossman = Bossman(args.repo, config) + return bossman + +class SetVerbosity(argparse.Action): + """ + Set logger verbosity when --verbosity argument is provided. + """ + def __init__(self, *args, **kwargs): + super(SetVerbosity, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + import logging + logging.basicConfig(level=values) + logger.info("SetVerbosity verbosity={verbosity}".format(verbosity=values)) diff --git a/bossman/cli/apply_cmd.py b/bossman/cli/apply_cmd.py new file mode 100644 index 0000000..f1489b6 --- /dev/null +++ b/bossman/cli/apply_cmd.py @@ -0,0 +1,20 @@ +from os import getcwd +import git +import argparse + +def init(subparsers: argparse._SubParsersAction): + parser = subparsers.add_parser("apply", help="apply local changes to remotes") + parser.set_defaults(func=exec) + +def exec(bossman, *args, **kwargs): + resources = bossman.get_resources() + for resource in resources: + print("checking {resource}...".format(resource=resource)) + status = bossman.get_resource_status(resource) + for changeset in reversed(status.missing_changesets): + print("applying {changeset}".format(changeset=changeset)) + for change in changeset.resource_changes.values(): + print("{resource}: applying {change}".format(resource=resource, change=change)) + bossman.apply_change(changeset, change) + else: + print("nothing left to do for {resource}".format(resource=resource)) diff --git a/bossman/cli/log_cmd.py b/bossman/cli/log_cmd.py new file mode 100644 index 0000000..c688567 --- /dev/null +++ b/bossman/cli/log_cmd.py @@ -0,0 +1,14 @@ +from os import getcwd +import git +import argparse +from bossman import Bossman +from bossman.resources import ResourceABC + +def init(subparsers: argparse._SubParsersAction): + parser = subparsers.add_parser("log", help="show resource change history") + parser.set_defaults(func=exec) + +def exec(bossman, *args, **kwargs): + changesets = bossman.get_changesets() + for changeset in changesets: + print(changeset) diff --git a/bossman/cli/status_cmd.py b/bossman/cli/status_cmd.py new file mode 100644 index 0000000..519f595 --- /dev/null +++ b/bossman/cli/status_cmd.py @@ -0,0 +1,14 @@ +from os import getcwd +import git +import argparse + +def init(subparsers: argparse._SubParsersAction): + parser = subparsers.add_parser("status", help="show resource status") + parser.set_defaults(func=exec) + +def exec(bossman, *args, **kwargs): + resources = bossman.get_resources() + #print("\n".join(resource.path for resource in resources)) + for resource in list(resources): + resource_status = bossman.get_resource_status(resource) + print(resource, resource_status) diff --git a/bossman/config.py b/bossman/config.py new file mode 100644 index 0000000..d8cb88f --- /dev/null +++ b/bossman/config.py @@ -0,0 +1,59 @@ +from os import getcwd +from os.path import expanduser, isabs, relpath, abspath, dirname, join +from bossman.errors import BossmanConfigurationError +from bossman.logging import logger + +logger = logger.getChild(__name__) + +def getenv(key, default=None): + from os import getenv + v = getenv(key) + if v == None: + return default + +_DEFAULT_RESOURCE_TYPES = ( + { + "module": "bossman.plugins.akamai.property", + "pattern": "akamai/property/{name}", + "options": { + "edgerc": getenv("AKAMAI_EDGERC", expanduser("~/.edgerc")), + "section": getenv("AKAMAI_EDGERC_SECTION", "papi"), + "switch_key": getenv("AKAMAI_EDGERC_SWITCHKEY", None), + } + }, +) + +class ResourceTypeConfig: + def __init__(self, data): + self.pattern = data.get("pattern") + self.module = data.get("module") + self.options = data.get("options", {}) + + def __str__(self): + return _dump(self) + +class Config: + def __init__(self, data): + self.resource_types = ( + ResourceTypeConfig(config) + for config + in data.get("resources", _DEFAULT_RESOURCE_TYPES) + ) + + def __str__(self): + return _dump(self) + +def _dump(obj): + def simplify(obj): + try: + __dict__ = object.__getattribute__(obj, "__dict__") + return dict( + (k, simplify(v)) + for (k, v) + in __dict__.items() + ) + except AttributeError: + return obj + return str(simplify(obj)) + + \ No newline at end of file diff --git a/bossman/errors.py b/bossman/errors.py new file mode 100644 index 0000000..1b1930e --- /dev/null +++ b/bossman/errors.py @@ -0,0 +1,20 @@ + +class BossmanRuntimeError(RuntimeError): + """ + Base class for bossman-specific runtime errors. + """ + pass + +class BossmanConfigurationError(BossmanRuntimeError): + """ + Configuration errors indicate that improper options have been passed to bossman. + """ + +class MultipleMatchingPluginsError(BossmanRuntimeError): + """ + Bossman matches plugins to resources using glob patterns. + """ + def __init__(self, resource, plugins): + super(MultipleMatchingPluginsError, self).__init__("Only one plugin should match a resource.") + self.resource = resource + self.plugins = plugins diff --git a/bossman/git.py b/bossman/git.py new file mode 100644 index 0000000..6296e2e --- /dev/null +++ b/bossman/git.py @@ -0,0 +1,35 @@ +from git import Repo, NULL_TREE +from os import getcwd +from types import SimpleNamespace + +def get_repo(workingDir=getcwd()): + return Repo(workingDir) + +def get_head_info(): + repo = get_repo() + return SimpleNamespace( + branch=repo.head.reference.name, + commit=repo.head.commit.hexsha, + message=repo.head.commit.message, + ) + +def print_commit_tree(commit="HEAD"): + repo = get_repo() + commit = repo.rev_parse(commit) + def visit(blob, depth=0): + if type(blob) == Tree: + print(" " * depth + blob.name) + for child in blob: + visit(child, depth+1) + elif type(blob) == Blob: + print(" " * depth + blob.name) + visit(commit.tree) + +# >>> repo.is_ancestor("88bfd2eb3c939bcf5409b003026c22a4fe9c8fa2", "9a7df4dc079ac844f96b2f74a501ab3b84ddcad1") +# True + +# >>> repo.rev_parse("1f8bbdec8e0d42262d273cd9bbaa217a85573db6") +# + +# >>> repo.head.commit.diff("1f8bbdec8e0d42262d273cd9bbaa217a85573db6", "build/property") +# [] diff --git a/bossman/logging.py b/bossman/logging.py new file mode 100644 index 0000000..7378d23 --- /dev/null +++ b/bossman/logging.py @@ -0,0 +1,16 @@ +import logging + +_console_formatter = logging.Formatter("%(message)s") + +_console_handler = logging.StreamHandler() +_console_handler.setFormatter(_console_formatter) + +logger = logging.getLogger() + +def get_class_logger(instance): + return logger.getChild( + "{module_name}.{class_name}".format( + module_name=instance.__class__.__module__, + class_name=instance.__class__.__name__ + ) + ) \ No newline at end of file diff --git a/bossman/plugins/__init__.py b/bossman/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bossman/plugins/akamai/__init__.py b/bossman/plugins/akamai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bossman/plugins/akamai/property.py b/bossman/plugins/akamai/property.py new file mode 100644 index 0000000..dcd445b --- /dev/null +++ b/bossman/plugins/akamai/property.py @@ -0,0 +1,110 @@ +import re +import json +from os import getenv +from os.path import expanduser, basename, dirname, join + +from bossman.abc.resource_type import ResourceTypeABC +from bossman.abc.resource import ResourceABC +from bossman.changes import Change +from bossman.plugins.akamai.lib.papi import PAPIClient +from bossman.changes import Change, ChangeSet + +RE_COMMIT = re.compile("^commit: ([a-z0-9]*)", re.MULTILINE) + +class PropertyResource(ResourceABC): + def __init__(self, path, **kwargs): + super(PropertyResource, self).__init__(path) + self.name = kwargs.get("name") + + @property + def rules_path(self): + return join(self.path, "rules.json") + + @property + def hostnames_path(self): + return join(self.path, "hostnames.json") + + @property + def paths(self): + return (self.rules_path, self.hostnames_path) + + +class ResourceTypeOptions: + def __init__(self, options): + self.edgerc = options.get("edgerc", expanduser("~/.edgerc")) + self.section = options.get("section", "papi") + self.switch_key = options.get("switch_key", None) + +class ResourceType(ResourceTypeABC): + def __init__(self, config): + super(ResourceType, self).__init__(config) + self.options = ResourceTypeOptions(config.options) + self.papi = PAPIClient(self.options.edgerc, self.options.section) + + def create_resource(self, path: str, **kwargs): + return PropertyResource(path, **kwargs) + + def get_remote_rev(self, resource: ResourceABC) -> str: + rev = None + property_id = self.papi.get_property_id(resource.name) + property_version = self.papi.find_latest_property_version( + property_id, + lambda v: RE_COMMIT.search(v.get("note", "")) + ) + self.logger.info("get_remote_rev {property_name} -> {property_version}".format(property_name=resource.name, property_version=property_version)) + if property_version: + rev = RE_COMMIT.search(property_version.get("note")).group(1) + return rev + + def is_dirty(self, resource: ResourceABC) -> bool: + """ + An Akamai property is dirty if its latest version does not refer to a + revision in its notes. + """ + is_dirty = True + property_id = self.papi.get_property_id(resource.name) + property_version = self.papi.get_latest_property_version(property_id) + if property_version: + is_dirty = not bool(RE_COMMIT.search(property_version.get("note"))) + return is_dirty + + def apply(self, changeset: ChangeSet, change: Change) -> bool: + changes = self.get_concrete_changes(change) + if len(changes): + self.logger.info("apply {rev} {resource} ({changes})".format( + rev=changeset.rev, + resource=change.resource, + changes=", ".join(changes.keys()) + )) + property_id = self.papi.get_property_id(change.resource.name) + latest_version = self.papi.get_latest_property_version(property_id) + next_version = self.papi.create_property_version(property_id, latest_version.get("propertyVersion")) + rule_tree = changes.get("rules", None) + if rule_tree: + rule_tree.update(comments=get_property_notes(changeset)) + self.papi.update_property_rule_tree(property_id, next_version.get("propertyVersion"), rule_tree) + hostnames = changes.get("hostnames", None) + if hostnames: + self.papi.update_property_hostnames(property_id, next_version.get("propertyVersion"), hostnames) + + def get_concrete_changes(self, change: Change) -> dict: + changes = dict() + for diff in change.diffs: + if diff.change_type in "RAM": + if basename(diff.b_path) == "hostnames.json": + changes.update(hostnames=json.loads(diff.b_blob.data_stream.read())) + elif basename(diff.b_path) == "rules.json": + changes.update(rules=json.loads(diff.b_blob.data_stream.read())) + return changes + +def get_property_notes(changeset: ChangeSet): + from textwrap import dedent + return dedent( + """\ + {message} + commit: {commit}\ + """ + ).format( + message=changeset.message, + commit=changeset.rev + ) diff --git a/bossman/repo.py b/bossman/repo.py new file mode 100644 index 0000000..20f1205 --- /dev/null +++ b/bossman/repo.py @@ -0,0 +1,75 @@ +import git +from bossman.logging import get_class_logger + +def true(*args, **kwargs): + return True + +def short_rev(rev): + return rev[:8] if rev else rev + +class Diff: + def __init__(self, diff: git.Diff): + self.change_type = diff.change_type + self.a_path = diff.a_path + self.a_blob = diff.a_blob + self.b_path = diff.b_path + self.b_blob = diff.b_blob + +class Commit: + def __init__(self, commit: git.Commit): + self.rev = short_rev(commit.hexsha) + self.date = commit.committed_datetime + self.message = commit.message + self.author = commit.author + self.diffs = [] + +class Repo: + def __init__(self, root): + self.logger = get_class_logger(self) + self._repo = git.Repo(root) + + def get_paths(self, rev: str = "HEAD", predicate = true) -> list: + """ + Lists the paths versioned for revision {rev}, optionally filtered + by {predicate}. + """ + commit = self._repo.rev_parse(rev) + paths = [] + def visit(blob, visited): + if blob.path in visited: + return # avoid infinite recursion + visited.add(blob.path) + if type(blob) == git.Tree: + for child in blob: + visit(child, visited) + elif type(blob) == git.Blob: + if predicate(blob.path): + # the repository likely contains non-resource files too + paths.append(blob.path) + visit(commit.tree, set()) + return paths + + def get_last_change_rev(self, paths: list, rev: str = "HEAD") -> str: + try: + commit = next(self._repo.iter_commits(rev, paths=paths)) + return short_rev(commit.hexsha) + except StopIteration: + return None + + def get_commits(self, since_rev: str = None, until_rev: str = "HEAD", paths: list = None) -> list: + commitRange = ("{since_rev}..{until_rev}".format(since_rev=since_rev, until_rev=until_rev) + if since_rev + else until_rev) + commits = self._repo.iter_commits(commitRange, paths=paths) + diffSets = [] + for commit in commits: + prev = git.NULL_TREE + if commit.parents: + prev = commit.parents[0] + diffs = commit.diff(prev, paths=paths, R=True) # R=True -> reverse + if len(diffs): + diffSet = Commit(commit) + for diff in diffs: + diffSet.diffs.append(Diff(diff)) + diffSets.append(diffSet) + return diffSets diff --git a/bossman/resources.py b/bossman/resources.py new file mode 100644 index 0000000..90d33cd --- /dev/null +++ b/bossman/resources.py @@ -0,0 +1,45 @@ +from types import SimpleNamespace +from bossman.repo import Repo +from bossman.abc.resource_type import ResourceTypeABC +from bossman.abc.resource import ResourceABC + +class ResourceStatus: + def __init__(self, local_rev, remote_rev, dirty, missing_changesets): + self.local_rev = local_rev + self.remote_rev = remote_rev + self.dirty = dirty + self.missing_changesets = missing_changesets + + def __str__(self): + return "local_rev={local_rev} remote_rev={remote_rev} dirty={dirty} changes={changes}".format( + local_rev=self.local_rev, + remote_rev=self.remote_rev, + dirty=self.dirty, + changes=len(self.missing_changesets) + ) + +class ResourceManager: + def __init__(self): + self.resource_types = list() + + def register_resource_type(self, resource_type: ResourceTypeABC): + self.resource_types.append(resource_type) + + def get_resource_type(self, path: str) -> ResourceTypeABC: + for resource_type in self.resource_types: + if resource_type.match(path): + return resource_type + return None + + def get_resource(self, path: str) -> ResourceABC: + resource_type = self.get_resource_type(path) + if resource_type: + return resource_type.get_resource(path) + return None + + def get_resources(self, repo: Repo, rev: str = "HEAD") -> list: + paths = repo.get_paths(rev) + resources = [] + for resource_type in self.resource_types: + resources.extend(resource_type.get_resources(paths)) + return resources diff --git a/main.py b/main.py new file mode 100644 index 0000000..72962f9 --- /dev/null +++ b/main.py @@ -0,0 +1,6 @@ +import sys +from os import path +sys.path.append(path.dirname(__file__)) + +import bossman.cli +bossman.cli.main() diff --git a/old.py b/old.py new file mode 100644 index 0000000..f768a5d --- /dev/null +++ b/old.py @@ -0,0 +1,102 @@ +import sys +from os.path import dirname, join + +sys.path.append(dirname(__file__)) + + + + +from src.logger import logger + + +def get_property_notes(): + from textwrap import dedent + from src.git import get_head_info + head = get_head_info() + return dedent( + """\ + {message} + commit: {commit}\ + """ + ).format( + message=head.message, + commit=head.commit, + branch=head.branch + ) + +def get_latest_git_version(propertyName): + from src.papi import PAPIClient + import re + papi = PAPIClient("/Users/ahogg/.edgerc", "lvm") + propertyId = papi.get_property_id(propertyName) + latestGitVersion = papi.find_latest_property_version( + propertyId, + lambda v: RE_PROPERTY_NOTE_COMMIT.search(v.get("note", "")) + ) + return latestGitVersion + + +def get_latest_version(propertyName): + from src.papi import PAPIClient + papi = PAPIClient("/Users/ahogg/.edgerc", "lvm") + propertyId = papi.get_property_id(propertyName) + latestVersion = papi.get_latest_property_version(propertyId) + return latestVersion + +class PropertyStatus: + def __init__(self, propertyName): + self.name = propertyName + self.latestVersion = None + self.latestCommit = None + self.latestCommitVersion = None + self.dirty = True + self.outdated = False + +def get_property_rule_tree(propertyName): + from os.path import join + import json + ruleTreePath = join("build", "property", propertyName, "rules.json") + return json.load(open(ruleTreePath, "r")) + +def get_property_status(propertyName): + status = PropertyStatus(propertyName) + latestVersion = get_latest_version(propertyName) + status.latestVersion = latestVersion.get("propertyVersion") + latestGitVersion = get_latest_git_version(propertyName) + if latestGitVersion: + status.latestCommit = RE_PROPERTY_NOTE_COMMIT.search(latestGitVersion.get("note")).group(1) + status.dirty = latestVersion.get("propertyVersion") != latestGitVersion.get("propertyVersion") + return status + +def update_property(propertyName): + def really_update_property(): + papi = PAPIClient("/Users/ahogg/.edgerc", "lvm") + propertyId = papi.get_property_id(propertyName) + ruleTree = get_property_rule_tree(propertyName) + ruleTree.update(comments=get_property_notes()) + version = papi.create_property_version(propertyId, status.latestCommitVersion if status.latestCommitVersion else status.latestVersion) + papi.update_property_rule_tree(propertyId, version.get("propertyVersion"), ruleTree) + + from src.papi import PAPIClient + status = get_property_status(propertyName) + if status.dirty: + logger.warning("property {propertyName} is dirty".format(propertyName=propertyName)) + really_update_property() + elif status.outdated: + really_update_property() + else: + print("nothing to do") + +update_property("lvm-static_jsonnettest") + +# def get_changes(dir): +# repo = get_git_repo() +# return [ +# (diff.change_type, diff.b_path if diff.b_path else diff.a_path) +# for diff in repo.head.commit.diff(None) +# if diff.a_mode != None and diff.b_mode != None and diff.b_path.startswith(dir) +# ] +# changes = get_changes("build/") +# print(changes) + +# print(get_property_notes()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c266ac3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +edgegrid-python>=1.1.1 +requests>=2.3.0 +gitpython +pyyaml +parse