Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref: Pin docker compose version for users #68

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion devservices/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import os

MINIMUM_DOCKER_COMPOSE_VERSION = "2.21.0"
MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
DEVSERVICES_DIR_NAME = "devservices"
CONFIG_FILE_NAME = "config.yml"
DOCKER_USER_PLUGIN_DIR = os.path.expanduser("~/.docker/cli-plugins/")

DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
DEVSERVICES_LOCAL_DEPENDENCIES_DIR = os.path.join(DEVSERVICES_LOCAL_DIR, "dependencies")
Expand Down
4 changes: 2 additions & 2 deletions devservices/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ class ConfigParseError(ConfigError):
pass


class DockerComposeVersionError(Exception):
"""Raised when the Docker Compose version is unsupported."""
class DockerComposeInstallationError(Exception):
"""Raised when the Docker Compose installation fails."""

pass

Expand Down
124 changes: 111 additions & 13 deletions devservices/utils/docker_compose.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
from __future__ import annotations

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_USER_PLUGIN_DIR
from devservices.constants import MINIMUM_DOCKER_COMPOSE_VERSION
from devservices.exceptions import DockerComposeError
from devservices.exceptions import DockerComposeVersionError
from devservices.exceptions import DockerComposeInstallationError
from devservices.utils.dependencies import install_dependencies
from devservices.utils.dependencies import verify_local_dependencies
from devservices.utils.services import Service
Expand All @@ -36,6 +42,94 @@ def get_active_docker_compose_projects() -> list[str]:
return running_projects.split("\n")[:-1]


def install_docker_compose() -> None:
# Determine the platform
system = platform.system()
machine = platform.machine()

# Map machine architecture to Docker's naming convention
arch_map = {
"x86_64": "x86_64",
"AMD64": "x86_64",
"arm64": "aarch64",
"aarch64": "aarch64",
"ARM64": "aarch64",
}
IanWoodard marked this conversation as resolved.
Show resolved Hide resolved

arch = arch_map.get(machine)
if not arch:
raise DockerComposeInstallationError(f"Unsupported architecture: {machine}")

# Determine the download URL based on the platform
if system == "Linux":
binary_name = f"docker-compose-linux-{arch}"
elif system == "Darwin":
binary_name = 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")

# 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}..."
)
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}"
)

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

# Verify the installation
try:
version = subprocess.run(
["docker", "compose", "version", "--short"], capture_output=True, text=True
).stdout
except Exception as e:
raise DockerComposeInstallationError(
f"Failed to verify Docker Compose installation: {e}"
)

print(f"Verified Docker Compose installation: v{version}")


def check_docker_compose_version() -> None:
cmd = ["docker", "compose", "version", "--short"]
try:
Expand All @@ -46,17 +140,14 @@ def check_docker_compose_version() -> None:
text=True,
check=True,
)

except subprocess.CalledProcessError as e:
raise DockerComposeError(
command=" ".join(cmd),
returncode=e.returncode,
stdout=e.stdout,
stderr=e.stderr,
except subprocess.CalledProcessError:
result = None
print(
f"Docker Compose is not installed, attempting to install v{MINIMUM_DOCKER_COMPOSE_VERSION}"
)

# Extract the version number from the output
version_output = result.stdout.strip()
version_output = result.stdout.strip() if result is not None else ""

# Use regex to find the version number
pattern = r"^(\d+\.\d+\.\d+)"
Expand All @@ -69,13 +160,20 @@ def check_docker_compose_version() -> None:
docker_compose_version = None

if docker_compose_version is None:
raise DockerComposeVersionError("Unable to detect docker compose version")
elif version.parse(docker_compose_version) < version.parse(
print(
f"Unable to detect Docker Compose version, attempting to install v{MINIMUM_DOCKER_COMPOSE_VERSION}"
)
elif version.parse(docker_compose_version) != version.parse(
MINIMUM_DOCKER_COMPOSE_VERSION
):
raise DockerComposeVersionError(
f"Docker compose version unsupported, please upgrade to >= {MINIMUM_DOCKER_COMPOSE_VERSION}"
print(
f"Docker Compose version v{docker_compose_version} unsupported, attempting to install v{MINIMUM_DOCKER_COMPOSE_VERSION}"
)
elif version.parse(docker_compose_version) == version.parse(
MINIMUM_DOCKER_COMPOSE_VERSION
):
return
install_docker_compose()


def run_docker_compose_command(
Expand Down
Loading