Skip to content

Commit

Permalink
feat(core): Add core.required_version parameter to enforce project …
Browse files Browse the repository at this point in the history
…ddb version requirement (#75)

Close #75
  • Loading branch information
Toilal committed Dec 17, 2020
1 parent 8764d01 commit bbf32e6
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 12 deletions.
3 changes: 3 additions & 0 deletions ddb/__main__.py
Expand Up @@ -15,6 +15,7 @@

from ddb.action import actions
from ddb.action.action import EventBinding, Action, WatchSupport
from ddb.action.runner import ExpectedError
from ddb.action.runnerfactory import action_event_binding_runner_factory
from ddb.binary import binaries
from ddb.cache import caches, global_cache_name, requests_cache_name, \
Expand Down Expand Up @@ -393,6 +394,8 @@ def on_config_reloaded():
events.main.terminate(command=command)

return context.exceptions
except ExpectedError as exception:
return [exception]
finally:
if not reset_disabled:
reset()
Expand Down
33 changes: 29 additions & 4 deletions ddb/action/runner.py
Expand Up @@ -11,6 +11,28 @@
A = TypeVar('A', bound=Action) # pylint:disable=invalid-name


class FailFastError(Exception):
"""
A base exception that should always fail fast, even when flag is not enabled.
"""


class ExpectedError(Exception):
"""
A base exception that can display it's own message in logs.
"""

def log_error(self):
"""
Log the error
:return:
"""
log_error = context.log.exception if \
context.log.isEnabledFor(logging.DEBUG) or config.args.exceptions \
else context.log.error
log_error(str(self))


# pylint:disable=too-few-public-methods


Expand Down Expand Up @@ -60,7 +82,7 @@ def run(self, *args, **kwargs): # pylint:disable=missing-function-docstring
return True
except Exception as exception: # pylint:disable=broad-except
self._handle_exception(exception)
if self.fail_fast: # TODO: Ajouter une option à la ligne de commande
if self.fail_fast or isinstance(exception, FailFastError):
raise

finally:
Expand All @@ -82,9 +104,12 @@ def _handle_exception(exception: Exception):
log_error = context.log.exception if \
context.log.isEnabledFor(logging.DEBUG) or config.args.exceptions \
else context.log.error
log_error("An unexpected error has occured %s: %s",
context.stack,
str(exception).strip())
if isinstance(exception, ExpectedError):
exception.log_error()
else:
log_error("An unexpected error has occured %s: %s",
context.stack,
str(exception).strip())
if isinstance(exception, CalledProcessError):
if exception.stderr:
for err_line in exception.stderr.decode("utf-8").splitlines():
Expand Down
7 changes: 4 additions & 3 deletions ddb/feature/core/__init__.py
Expand Up @@ -6,7 +6,7 @@
from dotty_dict import Dotty

from .actions import FeaturesAction, ConfigAction, ReloadConfigAction, EjectAction, SelfUpdateAction, \
MainCheckForUpdateAction, VersionAction
CheckForUpdateAction, VersionAction, CheckRequiredVersion
from .schema import CoreFeatureSchema
from ..feature import Feature, FeatureConfigurationAutoConfigureError
from ..schema import FeatureSchema
Expand Down Expand Up @@ -44,8 +44,9 @@ def actions(self) -> Iterable[Action]:
ReloadConfigAction(),
EjectAction(),
SelfUpdateAction(),
MainCheckForUpdateAction(),
VersionAction()
CheckForUpdateAction(),
VersionAction(),
CheckRequiredVersion()
)

@property
Expand Down
57 changes: 54 additions & 3 deletions ddb/feature/core/actions.py
Expand Up @@ -21,6 +21,7 @@
from ddb.utils.table_display import get_table_display
from .. import features
from ...action.action import EventBinding
from ...action.runner import FailFastError, ExpectedError
from ...command import Command
from ...config.flatten import flatten
from ...context import context
Expand Down Expand Up @@ -104,8 +105,9 @@ def _build_update_header(last_release):

def _build_update_details(github_repository, last_release):
row = []
if is_binary():
row.append('run "ddb self-update" command to update.')
update_tip = _build_update_tip()
if update_tip:
row.append(update_tip)
row.extend((
'For more information, check the following links:',
'https://github.com/{}/releases/tag/{}'.format(github_repository, last_release),
Expand All @@ -114,6 +116,12 @@ def _build_update_details(github_repository, last_release):
return row


def _build_update_tip():
if is_binary():
return 'run "ddb self-update" command to update.'
return ''


def is_binary():
"""
Check if current process is binary.
Expand Down Expand Up @@ -304,7 +312,7 @@ def execute(silent: bool):
print_version(github_repository, silent)


class MainCheckForUpdateAction(InitializableAction):
class CheckForUpdateAction(InitializableAction):
"""
Check if a new version is available on github.
"""
Expand Down Expand Up @@ -339,6 +347,49 @@ def execute(command: Command):
cache.set('last_check', today)


class RequiredVersionError(FailFastError, ExpectedError):
"""
Exception that should be raised when the current version doesn't fullfil the required one.
"""

def log_error(self):
context.log.error(str(self))


class CheckRequiredVersion(Action):
"""
Check if a new version is available on github.
"""

@property
def name(self) -> str:
return "core:check-required-version"

@property
def event_bindings(self):
return events.main.start

@staticmethod
def execute(command: Command):
"""
Check for updates
:param command command name
:return:
"""
if command.name not in ['self-update']:
required_version = config.data.get('core.required_version')
if not required_version:
return
if required_version > get_current_version():
update_tip = _build_update_tip()
if update_tip:
update_tip = ' ' + update_tip
raise RequiredVersionError(
"This project requires ddb {}+. Current version is {}.{}".format(required_version,
get_current_version(),
update_tip))


class SelfUpdateAction(Action):
"""
Self update ddb if a newer version is available.
Expand Down
1 change: 1 addition & 0 deletions ddb/feature/core/schema.py
Expand Up @@ -66,3 +66,4 @@ class CoreFeatureSchema(FeatureSchema):
process = fields.Dict(fields.String(), fields.Nested(ProcessSchema()), default={}) # Process binary mappings
configuration = fields.Nested(ConfigurationSchema(), default=ConfigurationSchema())
github_repository = fields.String(required=True, default="gfi-centre-ouest/docker-devbox-ddb")
required_version = fields.String(required=False, allow_none=True, default=None)
7 changes: 7 additions & 0 deletions docs/features/core.md
Expand Up @@ -42,6 +42,12 @@ Feature configuration
- `project.name`: The name of the project
- type: string
- default: <the name of the project directory>
- `required_version`: The minimal required ddb version for the project to work properly. If the required version is higher than the currently running one, ddb will stop working until it's updated.
- type: string
- default: null
- `github_repository`: Github repository used to check new release of ddb. Should not be changed.
- type: string
- default: 'gfi-centre-ouest/docker-devbox-ddb'

!!! example "Configuration"
```yaml
Expand All @@ -57,6 +63,7 @@ Feature configuration
- ci
- dev
current: dev
github_repository: gfi-centre-ouest/docker-devbox-ddb
os: posix
path:
ddb_home: /home/devbox/.docker-devbox/ddb
Expand Down
2 changes: 2 additions & 0 deletions tests/it/test_core.data/required-version/ddb.yml
@@ -0,0 +1,2 @@
core:
required_version: 1.3.0
1 change: 1 addition & 0 deletions tests/it/test_core.data/required-version/test.jinja
@@ -0,0 +1 @@
{{core.env.current}}
57 changes: 55 additions & 2 deletions tests/it/test_core.py
@@ -1,3 +1,4 @@
import os
from pathlib import Path

from _pytest.capture import CaptureFixture
Expand All @@ -18,7 +19,7 @@ def test_self_update_no_binary(self, project_loader, capsys: CaptureFixture, moc

main(["self-update"])

assert Path('./bin/ddb').read_text() == "binary"
assert Path('./bin/ddb').read_bytes() == b"binary"

outerr = capsys.readouterr()
assert outerr.err == ""
Expand Down Expand Up @@ -119,7 +120,6 @@ def test_version_outdated_binary(self, project_loader, capsys: CaptureFixture, m
main(["--version"])

outerr = capsys.readouterr()
print(outerr.out)
assert outerr.err == ""
assert outerr.out == '''+---------------------------------------------------------------------------------------+
| ddb 1.3.0 |
Expand All @@ -134,3 +134,56 @@ def test_version_outdated_binary(self, project_loader, capsys: CaptureFixture, m
| Please report any bug or feature request at |
| https://github.com/gfi-centre-ouest/docker-devbox-ddb/issues |
+---------------------------------------------------------------------------------------+\n'''

def test_required_version_eq(self, project_loader, capsys: CaptureFixture, mocker: MockerFixture):
mocker.patch('ddb.feature.core.actions.get_current_version', lambda *args, **kwargs: '1.3.0')

project_loader("required-version")

exceptions = main(["configure"])

assert not exceptions
assert os.path.exists('test')

def test_required_version_lt(self, project_loader, capsys: CaptureFixture, mocker: MockerFixture):
mocker.patch('ddb.feature.core.actions.get_current_version', lambda *args, **kwargs: '1.2.9')

project_loader("required-version")

exceptions = main(["configure"])
assert len(exceptions) == 1
assert str(exceptions[0]) == "This project requires ddb 1.3.0+. Current version is 1.2.9."

outerr = capsys.readouterr()
assert "This project requires ddb 1.3.0+. Current version is 1.2.9." in outerr.err
assert outerr.out == ""

assert not os.path.exists('test')

def test_required_version_lt_binary(self, project_loader, capsys: CaptureFixture, mocker: MockerFixture):
mocker.patch('ddb.feature.core.actions.is_binary', lambda *args, **kwargs: True)
mocker.patch('ddb.feature.core.actions.get_current_version', lambda *args, **kwargs: '1.2.9')

project_loader("required-version")

exceptions = main(["configure"])
assert len(exceptions) == 1
assert str(exceptions[0]) == "This project requires ddb 1.3.0+. " \
"Current version is 1.2.9. " \
"run \"ddb self-update\" command to update."

outerr = capsys.readouterr()
assert "This project requires ddb 1.3.0+. Current version is 1.2.9." in outerr.err
assert outerr.out == ""

assert not os.path.exists('test')

def test_required_version_gt(self, project_loader, capsys: CaptureFixture, mocker: MockerFixture):
mocker.patch('ddb.feature.core.actions.get_current_version', lambda *args, **kwargs: '1.3.1')

project_loader("required-version")

exceptions = main(["configure"])

assert not exceptions
assert os.path.exists('test')

0 comments on commit bbf32e6

Please sign in to comment.