Skip to content

Commit

Permalink
feat(self-update): Add self-update command to update binary from gith…
Browse files Browse the repository at this point in the history
…ub (#131)

Close #131
  • Loading branch information
Toilal committed Dec 17, 2020
1 parent b4f1127 commit 0171f37
Show file tree
Hide file tree
Showing 12 changed files with 432 additions and 67 deletions.
47 changes: 11 additions & 36 deletions ddb/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import verboselogs
from colorlog import default_log_colors, ColoredFormatter
from ddb import __version__

from ddb.action import actions
from ddb.action.action import EventBinding, Action, WatchSupport
from ddb.action.runnerfactory import action_event_binding_runner_factory
Expand All @@ -29,12 +29,11 @@
from ddb.feature import features, Feature
from ddb.feature.bootstrap import reset_available_features, append_available_feature, \
load_bootstrap_config, bootstrap_register_features
from ddb.feature.core import ConfigureSecondPassException
from ddb.feature.selfupdate.actions import print_version, check_for_update
from ddb.phase import phases
from ddb.registry import Registry, RegistryObject
from ddb.service import services
from ddb.utils.release import ddb_repository, get_latest_release_version
from ddb.utils.table_display import get_table_display
from ddb.feature.core import ConfigureSecondPassException

_watch_started_event = threading.Event()
_watch_stop_event = threading.Event()
Expand Down Expand Up @@ -363,28 +362,7 @@ def main(args: Optional[Sequence[str]] = None,
raise

if config.args.version:
if config.args.silent:
print(__version__)
else:
version_title = 'ddb ' + __version__
version_content = [
[
'Please report any bug or feature request at',
'https://github.com/gfi-centre-ouest/docker-devbox-ddb/issues'
]
]

last_release = get_latest_release_version()
if last_release and __version__ < last_release:
version_content.append([
'',
'A new version is available : ' + last_release,
'https://github.com/' + ddb_repository + '/releases/tag/' + last_release,
'https://github.com/' + ddb_repository + '/blob/' + last_release + '/CHANGELOG.md',
''
])

print(get_table_display(version_title, version_content))
print_version(config.args.silent)
return []

load_registered_features(False)
Expand All @@ -411,7 +389,8 @@ def on_config_reloaded():

bus.on(events.config.reloaded.name, on_config_reloaded) # pylint:disable=no-member
handle_command_line(command)
if command.name not in ['activate', 'deactivate', 'run']:

if command.name not in ['activate', 'deactivate', 'run', 'self-update']:
_check_for_update()
return context.exceptions
finally:
Expand All @@ -434,21 +413,17 @@ def stop_watch():


def _check_for_update():
"""
Check for updates
:return:
"""
register_global_cache('core.version')
cache = caches.get('core.version')
last_check = cache.get('last_check', None)
today = date.today()

if last_check is None or last_check < today:
last_release = get_latest_release_version()
if last_release and __version__ < last_release:
header = 'A new version is available : {}'.format(last_release)
content = [[
'For more information, check the following links :',
'https://github.com/{}/releases/tag/{}'.format(ddb_repository, last_release),
'https://github.com/{}/releases/tag/{}/CHANGELOG.md'.format(ddb_repository, last_release),
]]
print(get_table_display(header, content))
check_for_update(True, True)

cache.set('last_check', today)

Expand Down
6 changes: 6 additions & 0 deletions ddb/event/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ def run(self):
Run phase
"""

@event("phase:selfupdate")
def selfupdate(self):
"""
Selfupdate phase
"""


class Config:
"""
Expand Down
2 changes: 2 additions & 0 deletions ddb/feature/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .jsonnet import JsonnetFeature
from .permissions import PermissionsFeature
from .run import RunFeature
from .selfupdate import SelfUpdateFeature
from .shell import ShellFeature
from .smartcd import SmartcdFeature
from .symlinks import SymlinksFeature
Expand All @@ -38,6 +39,7 @@
JsonnetFeature(),
PermissionsFeature(),
RunFeature(),
SelfUpdateFeature(),
SmartcdFeature(),
ShellFeature(),
SymlinksFeature(),
Expand Down
50 changes: 50 additions & 0 deletions ddb/feature/selfupdate/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from argparse import ArgumentParser
from typing import Iterable, ClassVar

from .actions import SelfUpdateAction
from .schema import SelfUpdateSchema
from ..feature import Feature
from ..schema import FeatureSchema
from ...action import Action
from ...command import Command, LifecycleCommand
from ...phase import DefaultPhase, Phase


class SelfUpdateFeature(Feature):
"""
Self update support.
"""

@property
def name(self) -> str:
return "selfupdate"

@property
def dependencies(self) -> Iterable[str]:
return ["core"]

@property
def schema(self) -> ClassVar[FeatureSchema]:
return SelfUpdateSchema

@property
def phases(self) -> Iterable[Phase]:
def configure_parser(parser: ArgumentParser):
parser.add_argument("--force", action="store_true", help="Force update")

return (
DefaultPhase("selfupdate", "Update ddb binary with latest version", parser=configure_parser),
)

@property
def commands(self) -> Iterable[Command]:
return (
LifecycleCommand("self-update", "Update ddb to latest version", "selfupdate"),
)

@property
def actions(self) -> Iterable[Action]:
return (
SelfUpdateAction(),
)
210 changes: 210 additions & 0 deletions ddb/feature/selfupdate/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
import os
import shutil
import sys
from pathlib import Path
from tempfile import NamedTemporaryFile
from urllib.error import HTTPError

import requests
from progress.bar import IncrementalBar

from ddb import __version__
from ddb.action import Action
from ddb.config import config
from ddb.event import events
from ddb.utils.table_display import get_table_display


def get_latest_release_version(github_repository: str):
"""
Retrieve latest release version from GitHub API
:param github_repository github repository to check
:return: Version from tag_name retrieved from GitHub API
"""
response = requests.get('https://api.github.com/repos/{}/releases/latest'.format(github_repository))
try:
response.raise_for_status()
tag_name = response.json().get('tag_name')
if tag_name and tag_name.startswith('v'):
tag_name = tag_name[1:]
return tag_name
except HTTPError: # pylint:disable=bare-except
return None


def get_current_version():
"""
Get the current version
:return:
"""
return __version__


def print_version(silent=False):
"""
Print the version and informations.
:return:
"""
if silent:
print(get_current_version())
return

version_title = 'ddb ' + get_current_version()
version_content = []

github_repository = config.data.get('selfupdate.github_repository')

last_release = get_latest_release_version(github_repository)

if last_release and get_current_version() < last_release:
version_content.append(_build_update_header(last_release))
version_content.append(_build_update_details(github_repository, last_release))
version_content.append([
'Please report any bug or feature request at',
'https://github.com/gfi-centre-ouest/docker-devbox-ddb/issues'
])
print(get_table_display(version_title, version_content))


def check_for_update(output=False, details=False):
"""
Check if a new version is available on github.
:param output: if True, new version information will be displayed.
:param details: if True, will display more details.
:return: True if an update is available.
"""
github_repository = config.data.get('selfupdate.github_repository')

last_release = get_latest_release_version(github_repository)

if last_release and get_current_version() < last_release:
if output:
header = _build_update_header(last_release)
if details:
row = _build_update_details(github_repository, last_release)
print(get_table_display(header, [row]))
else:
for row in header:
print(row)
return True
return False


def _build_update_header(last_release):
return ['A new version is available: {}'.format(last_release)]


def _build_update_details(github_repository, last_release):
row = []
if is_binary():
row.append('run "ddb self-update" command to update.')
row.extend((
'For more information, check the following links:',
'https://github.com/{}/releases/tag/{}'.format(github_repository, last_release),
'https://github.com/{}/releases/tag/{}/CHANGELOG.md'.format(github_repository, last_release),
))
return row


def is_binary():
"""
Check if current process is binary.
:return:
"""
return getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')


def get_binary_path():
"""
Get the binary path
:return:
"""
return sys.argv[0]


def get_binary_destination_path(binary_path: str):
"""
Get binary path destination
:param binary_path:
:return:
"""
if binary_path.endswith('.py') \
and Path(binary_path).read_text().startswith("#!/usr/bin/env python"):
# Avoid removing main source file when running on development.
binary_path = binary_path[:-3] + ".bin"
return binary_path


class SelfUpdateAction(Action):
"""
Self update ddb if a newer version is available.
"""

@property
def name(self) -> str:
return "selfupdate:update"

@property
def event_bindings(self):
return events.phase.selfupdate

def execute(self):
"""
Execute action
"""
github_repository = config.data.get('selfupdate.github_repository')

if not is_binary():
print('ddb is running from a package mode than doesn\'t support self-update.')
print(
'You can download binary package supporting it ' +
'from github: https://github.com/{}/releases'.format(github_repository)
)
return

last_release = get_latest_release_version(github_repository)

if not check_for_update(True):
print('ddb is already up to date.')
if not config.args.force:
return

self.self_update_binary(github_repository, last_release)

@staticmethod
def self_update_binary(github_repository, version):
"""
Self update the ddb binary
:param github_repository:
:param version:
:return:
"""
binary_path = get_binary_path()

if not os.access(binary_path, os.W_OK):
raise PermissionError("You don't have permission to write on ddb binary file. ({})".format(sys.argv[0]))

remote_filename = 'ddb.exe' if os.name == 'nt' else 'ddb'
url = 'https://github.com/{}/releases/download/v{}/{}'.format(github_repository, version, remote_filename)

progress_bar = None
with requests.get(url, stream=True) as response:
response.raise_for_status()

with NamedTemporaryFile() as tmp:
if not progress_bar:
content_length = int(response.headers['content-length'])
progress_bar = IncrementalBar('Downloading', max=content_length)

for chunk in response.iter_content(32 * 1024):
progress_bar.next(len(chunk)) # pylint:disable=not-callable
tmp.write(chunk)
tmp.flush()

binary_path = get_binary_destination_path(binary_path)
shutil.copyfile(tmp.name, binary_path)

progress_bar.finish()

print("ddb has been updated.")
10 changes: 10 additions & 0 deletions ddb/feature/selfupdate/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from marshmallow import fields

from ddb.feature.schema import FeatureSchema


class SelfUpdateSchema(FeatureSchema):
"""
SelfUpdate schema.
"""
github_repository = fields.String(required=True, default="gfi-centre-ouest/docker-devbox-ddb")
Loading

0 comments on commit 0171f37

Please sign in to comment.