Skip to content

Commit

Permalink
feat(commands): Add update command (#75)
Browse files Browse the repository at this point in the history
* add update command
  • Loading branch information
hubertdeng123 authored Oct 21, 2024
1 parent 93cfd42 commit 2e3e906
Show file tree
Hide file tree
Showing 11 changed files with 408 additions and 94 deletions.
14 changes: 14 additions & 0 deletions devservices/commands/check_for_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from __future__ import annotations

import json
from urllib.request import urlopen


def check_for_update(current_version: str) -> str | None:
url = "https://api.github.com/repos/getsentry/devservices/releases/latest"
with urlopen(url) as response:
if response.status == 200:
data = json.loads(response.read().decode("utf-8"))
latest_version = str(data["tag_name"])
return latest_version
return None
66 changes: 66 additions & 0 deletions devservices/commands/update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import annotations

import platform
import sys
from argparse import _SubParsersAction
from argparse import ArgumentParser
from argparse import Namespace
from importlib import metadata

from devservices.commands.check_for_update import check_for_update
from devservices.constants import DEVSERVICES_DOWNLOAD_URL
from devservices.exceptions import BinaryInstallError
from devservices.exceptions import DevservicesUpdateError
from devservices.utils.install_binary import install_binary


def is_in_virtualenv() -> bool:
return hasattr(sys, "real_prefix") or (
hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix
)


def update_version(exec_path: str, latest_version: str) -> None:
system = platform.system().lower()
url = f"{DEVSERVICES_DOWNLOAD_URL}/{latest_version}/devservices-{system}"
try:
install_binary("devservices", exec_path, latest_version, url)
except BinaryInstallError as e:
raise DevservicesUpdateError(f"Failed to update devservices: {e}")

print(f"Devservices {latest_version} updated successfully")


def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
parser = subparsers.add_parser(
"update", help="update devservices to the latest version"
)
parser.set_defaults(func=update)


def update(args: Namespace) -> None:
current_version = metadata.version("devservices")
latest_version = check_for_update(current_version)

if latest_version is None:
raise DevservicesUpdateError("Failed to check for updates.")

if latest_version == current_version:
print("You're already on the latest version.")
return

print(f"A new version of devservices is available: {latest_version}")

if is_in_virtualenv():
print("You are running in a virtual environment.")
print(
"To update, please update your requirements.txt or requirements-dev.txt file with the new version."
)
print(
f"For example, update the line in requirements.txt to: devservices=={latest_version}"
)
print("Then, run: pip install --update -r requirements.txt")
return

print("Upgrading to the latest version...")
update_version(sys.executable, latest_version)
4 changes: 4 additions & 0 deletions devservices/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,7 @@
"extensions.partialClone": "true",
"core.sparseCheckout": "true",
}

DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
BINARY_PERMISSIONS = 0o755
14 changes: 13 additions & 1 deletion devservices/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ class ConfigParseError(ConfigError):
pass


class DockerComposeInstallationError(Exception):
class BinaryInstallError(Exception):
"""Raised when a binary cannot be installed."""

pass


class DevservicesUpdateError(BinaryInstallError):
"""Raised when the devservices update fails."""

pass


class DockerComposeInstallationError(BinaryInstallError):
"""Raised when the Docker Compose installation fails."""

pass
Expand Down
11 changes: 11 additions & 0 deletions devservices/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from devservices.commands import start
from devservices.commands import status
from devservices.commands import stop
from devservices.commands import update
from devservices.commands.check_for_update import check_for_update
from devservices.utils.docker_compose import check_docker_compose_version

sentry_environment = (
Expand Down Expand Up @@ -58,6 +60,7 @@ def main() -> None:
list_services.add_parser(subparsers)
status.add_parser(subparsers)
logs.add_parser(subparsers)
update.add_parser(subparsers)

args = parser.parse_args()

Expand All @@ -68,6 +71,14 @@ def main() -> None:
else:
parser.print_help()

if args.command != "update":
newest_version = check_for_update(metadata.version("devservices"))
if newest_version != metadata.version("devservices"):
print(
f"\n\033[93mWARNING: A new version of devservices is available: {newest_version}\033[0m"
)
print("To update, run: \033[1mdevservices update\033[0m")


if __name__ == "__main__":
main()
71 changes: 20 additions & 51 deletions devservices/utils/docker_compose.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,24 @@
import os
import platform
import re
import shutil
import subprocess
import tempfile
import time
from typing import cast
from urllib.request import urlretrieve

from packaging import version

from devservices.constants import CONFIG_FILE_NAME
from devservices.constants import DEVSERVICES_DIR_NAME
from devservices.constants import DEVSERVICES_LOCAL_DEPENDENCIES_DIR
from devservices.constants import DEVSERVICES_LOCAL_DEPENDENCIES_DIR_KEY
from devservices.constants import DOCKER_COMPOSE_DOWNLOAD_URL
from devservices.constants import DOCKER_USER_PLUGIN_DIR
from devservices.constants import MINIMUM_DOCKER_COMPOSE_VERSION
from devservices.exceptions import BinaryInstallError
from devservices.exceptions import DockerComposeError
from devservices.exceptions import DockerComposeInstallationError
from devservices.utils.dependencies import install_dependencies
from devservices.utils.dependencies import verify_local_dependencies
from devservices.utils.install_binary import install_binary
from devservices.utils.services import Service


Expand Down Expand Up @@ -60,62 +59,32 @@ def install_docker_compose() -> None:
if not arch:
raise DockerComposeInstallationError(f"Unsupported architecture: {machine}")

binary_name = "docker-compose"

# Determine the download URL based on the platform
if system == "Linux":
binary_name = f"docker-compose-linux-{arch}"
binary_name_with_extension = f"docker-compose-linux-{arch}"
elif system == "Darwin":
binary_name = f"docker-compose-darwin-{arch}"
binary_name_with_extension = f"docker-compose-darwin-{arch}"
else:
raise DockerComposeInstallationError(f"Unsupported operating system: {system}")

url = f"https://github.com/docker/compose/releases/download/v{MINIMUM_DOCKER_COMPOSE_VERSION}/{binary_name}"

# Create a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
temp_file = os.path.join(temp_dir, "docker-compose")
url = f"{DOCKER_COMPOSE_DOWNLOAD_URL}/v{MINIMUM_DOCKER_COMPOSE_VERSION}/{binary_name_with_extension}"
destination = os.path.join(DOCKER_USER_PLUGIN_DIR, binary_name)

# Download the Docker Compose binary with retries
max_retries = 3
retry_delay_seconds = 1
print(
f"Downloading Docker Compose {MINIMUM_DOCKER_COMPOSE_VERSION} from {url}..."
try:
install_binary(
binary_name,
destination,
MINIMUM_DOCKER_COMPOSE_VERSION,
url,
)
for attempt in range(max_retries):
try:
urlretrieve(url, temp_file)
break
except Exception as e:
if attempt < max_retries - 1:
print(
f"Download failed. Retrying in {retry_delay_seconds} seconds... (Attempt {attempt + 1}/{max_retries})"
)
time.sleep(retry_delay_seconds)
else:
raise DockerComposeInstallationError(
f"Failed to download Docker Compose after {max_retries} attempts: {e}"
)

# Make the binary executable
try:
os.chmod(temp_file, 0o755)
except Exception as e:
raise DockerComposeInstallationError(
f"Failed to set executable permissions: {e}"
)

destination = os.path.join(DOCKER_USER_PLUGIN_DIR, "docker-compose")
os.makedirs(DOCKER_USER_PLUGIN_DIR, exist_ok=True)

try:
shutil.move(temp_file, destination)
except Exception as e:
raise DockerComposeInstallationError(
f"Failed to move Docker Compose binary to {destination}: {e}"
)
except BinaryInstallError as e:
raise DockerComposeInstallationError(f"Failed to install Docker Compose: {e}")

print(
f"Docker Compose {MINIMUM_DOCKER_COMPOSE_VERSION} installed successfully to {destination}"
)
print(
f"Docker Compose {MINIMUM_DOCKER_COMPOSE_VERSION} installed successfully to {destination}"
)

# Verify the installation
try:
Expand Down
52 changes: 52 additions & 0 deletions devservices/utils/install_binary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import os
import shutil
import tempfile
import time
from urllib.request import urlretrieve

from devservices.constants import BINARY_PERMISSIONS
from devservices.exceptions import BinaryInstallError


def install_binary(
binary_name: str,
exec_path: str,
version: str,
url: str,
) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
temp_file = os.path.join(temp_dir, binary_name)

# Download the binary with retries
max_retries = 3
retry_delay_seconds = 1
print(f"Downloading {binary_name} {version} from {url}...")
for attempt in range(max_retries):
try:
urlretrieve(url, temp_file)
break
except Exception as e:
if attempt < max_retries - 1:
print(
f"Download failed. Retrying in {retry_delay_seconds} seconds... (Attempt {attempt + 1}/{max_retries - 1})"
)
time.sleep(retry_delay_seconds)
else:
raise BinaryInstallError(
f"Failed to download {binary_name} after {max_retries} attempts: {e}"
)

# Make the binary executable
try:
os.chmod(temp_file, BINARY_PERMISSIONS)
except (PermissionError, FileNotFoundError) as e:
raise BinaryInstallError(f"Failed to set executable permissions: {e}")

try:
shutil.move(temp_file, exec_path)
except (PermissionError, FileNotFoundError) as e:
raise BinaryInstallError(
f"Failed to move {binary_name} binary to {exec_path}: {e}"
)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
--index-url https://pypi.devinfra.sentry.io/simple
pyyaml==6.0.1
packaging==24.0
sentry-devenv==1.8.0
sentry-sdk==2.14.0
Loading

0 comments on commit 2e3e906

Please sign in to comment.