Skip to content

Commit

Permalink
feat: adds command to update the CLI
Browse files Browse the repository at this point in the history
We currently do not have a built-in mechanism to update the CLI. One has
to install the new version via pip on their systems. This commit
introduces a command to update to the latest CLI version if the CLI was
installed via pip.

Usage: rio update [OPTIONS]

  Update the CLI to the latest version

Options:
  -f, --force, --silent  Skip confirmation
  --help                 Show this message and exit.
  • Loading branch information
pallabpain committed Aug 3, 2023
1 parent 19a06f8 commit c64c9d7
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 7 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ directory-tree = ">=0.0.3.1"
yaspin = ">=2.3.0"
jsonschema = ">=4.0.0"
waiting = ">=1.4.1"
semver = ">=3.0.0"

[requires]
python_version = "3"
10 changes: 9 additions & 1 deletion Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 41 additions & 2 deletions riocli/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from riocli.chart import chart
from riocli.completion import completion
from riocli.config import Configuration
from riocli.constants import Colors, Symbols
from riocli.deployment import deployment
from riocli.device import device
from riocli.disk import disk
Expand All @@ -42,15 +43,21 @@
from riocli.shell import shell, deprecated_repl
from riocli.static_route import static_route
from riocli.usergroup import usergroup
from riocli.utils import (
check_for_updates,
pip_install_cli,
is_pip_installation,
update_appimage,
)
from riocli.vpn import vpn


@with_plugins(iter_entry_points('riocli.plugins'))
@click.group(
invoke_without_command=False,
cls=HelpColorsGroup,
help_headers_color="yellow",
help_options_color="green",
help_headers_color=Colors.YELLOW,
help_options_color=Colors.GREEN,
)
@click.pass_context
def cli(ctx: Context, config: str = None):
Expand All @@ -75,6 +82,38 @@ def version():
return


@cli.command('update')
@click.option('-f', '--force', '--silent', 'silent', is_flag=True,
type=click.BOOL, default=False,
help="Skip confirmation")
def update(silent: bool) -> None:
"""
Update the CLI to the latest version
"""
available, latest = check_for_updates(__version__)
if not available:
click.secho('🎉 You are using the latest version', fg=Colors.GREEN)
return

click.secho('🎉 A newer version ({}) is available.'.format(latest),
fg=Colors.GREEN)

if not silent:
click.confirm('Do you want to update?', abort=True, default=False)

try:
if is_pip_installation():
pip_install_cli(version=latest)
else:
update_appimage(version=latest)
except Exception as e:
click.secho('{} Failed to update the CLI'.format(e), fg=Colors.RED)
raise SystemExit(1) from e

click.secho('{} Update successful!'.format(Symbols.SUCCESS),
fg=Colors.GREEN)


cli.add_command(apply)
cli.add_command(chart)
cli.add_command(explain)
Expand Down
108 changes: 105 additions & 3 deletions riocli/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import os
import random
import shlex
import string
import subprocess
import sys
import typing
from pathlib import Path
from shutil import get_terminal_size
from tempfile import TemporaryDirectory
from uuid import UUID

import click
import requests
import semver
import yaml
from click_help_colors import HelpColorsGroup
from munch import munchify
from tabulate import tabulate

from riocli.constants import Colors, Symbols


def inspect_with_format(obj: typing.Any, format_type: str):
if format_type == 'json':
Expand Down Expand Up @@ -81,11 +90,13 @@ def run_bash(cmd, bg=False) -> str:


def random_string(letter_count, digit_count):
str1 = ''.join((random.choice(string.ascii_letters) for x in range(letter_count)))
str1 = ''.join(
(random.choice(string.ascii_letters) for x in range(letter_count)))
str1 += ''.join((random.choice(string.digits) for x in range(digit_count)))

sam_list = list(str1) # it converts the string to list.
random.shuffle(sam_list) # It uses a random.shuffle() function to shuffle the string.
random.shuffle(
sam_list) # It uses a random.shuffle() function to shuffle the string.
final_string = ''.join(sam_list)
return final_string

Expand Down Expand Up @@ -118,7 +129,8 @@ def is_valid_uuid(uuid_to_test, version=4):
return str(uuid_obj) == uuid_to_test


def tabulate_data(data: typing.List[typing.List], headers: typing.List[str] = None):
def tabulate_data(data: typing.List[typing.List],
headers: typing.List[str] = None):
"""
Prints data in tabular format
"""
Expand All @@ -138,3 +150,93 @@ def print_separator(color: str = 'blue'):
"""
col, _ = get_terminal_size()
click.secho(" " * col, bg=color)


def is_pip_installation() -> bool:
return 'python' in sys.executable


def check_for_updates(current_version: str) -> tuple[bool, str]:
try:
package_info = requests.get(
'https://pypi.org/pypi/rapyuta-io-cli/json').json()
except Exception as e:
click.secho('Failed to fetch upstream package info: {}'.format(e),
fg=Colors.RED)
raise SystemExit(1) from e

upstream_version = package_info.get('info', {}).get('version')

current_version = semver.Version.parse(current_version)
available = semver.Version.parse(upstream_version).compare(current_version)

return available > 0, upstream_version


def pip_install_cli(
version: str,
force_reinstall: bool = False,
) -> subprocess.CompletedProcess:
"""
Installs the given rapyuta-io-cli version using pip
"""
if not version:
raise ValueError('version cannot by empty.')

try:
semver.Version.parse(version)
except ValueError as err:
raise err

package_name = 'rapyuta-io-cli=={}'.format(version)

# https://pip.pypa.io/en/latest/user_guide/#using-pip-from-your-program
command = [sys.executable, '-m', 'pip', 'install', package_name]
if force_reinstall:
command.append('--force-reinstall')

return subprocess.run(command, check=True)


def update_appimage(version: str):
"""
Updates the AppImage locally
"""
if not version:
raise ValueError('version cannot be empty')

if os.getuid() != 0:
click.secho(
'{} Please run this as the root user.'.format(Symbols.WARNING),
fg=Colors.YELLOW)
raise SystemExit(1)

# URL to get the latest release metadata
url = 'https://api.github.com/repos/rapyuta-robotics/rapyuta-io-cli/releases/latest'

try:
response = requests.get(url)
data = munchify(response.json())
except Exception as e:
click.secho('Failed to fetch release info: {}'.format(e),
fg=Colors.RED)
raise SystemExit(1) from e

asset = None
for a in data.get('assets', []):
if 'AppImage' in a.name and version in a.name:
asset = a
break

if asset is None:
raise Exception(
'Failed to retrieve the download URL for the latest AppImage')

with TemporaryDirectory() as tmp:
# Download and save the binary in a temp dir
response = requests.get(asset.browser_download_url)
save_to = Path(tmp) / 'rio'
save_to.write_bytes(response.content)
os.chmod(save_to, 0o755)
# Now replace the current executable with the new file
os.rename(save_to, sys.executable)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"directory-tree>=0.0.3.1",
"yaspin>=2.3.0",
"jsonschema>=4.0.0",
"waiting>=1.4.1"
"waiting>=1.4.1",
"semver>=3.0.0",
],
setup_requires=["flake8"],
)

0 comments on commit c64c9d7

Please sign in to comment.