Browse Source

ref: port /get-cli/ endpoint from getsentry (#79035)

once deployed this can be removed from `getsentry` (this is a noop for
SaaS until that is completed)

<!-- Describe your PR here. -->
anthony sottile 5 months ago
parent
commit
516a21ddd6
4 changed files with 191 additions and 0 deletions
  1. 2 0
      pyproject.toml
  2. 161 0
      src/sentry/web/frontend/cli.py
  3. 8 0
      src/sentry/web/urls.py
  4. 20 0
      tests/sentry/web/frontend/test_cli.py

+ 2 - 0
pyproject.toml

@@ -532,6 +532,7 @@ module = [
     "sentry.utils.uwsgi",
     "sentry.utils.zip",
     "sentry.web.frontend.auth_provider_login",
+    "sentry.web.frontend.cli",
     "sentry.web.frontend.csv",
     "sentry_plugins.base",
     "tests.sentry.deletions.test_group",
@@ -572,6 +573,7 @@ module = [
     "tests.sentry.tasks.test_on_demand_metrics",
     "tests.sentry.types.test_actor",
     "tests.sentry.types.test_region",
+    "tests.sentry.web.frontend.test_cli",
     "tools.*",
 ]
 disallow_any_generics = true

+ 161 - 0
src/sentry/web/frontend/cli.py

@@ -0,0 +1,161 @@
+from urllib.parse import quote_plus
+
+from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
+
+from sentry.silo.base import control_silo_function
+from sentry.utils import metrics
+
+SCRIPT = r"""#!/bin/sh
+set -eu
+
+# allow overriding the version
+VERSION=${SENTRY_CLI_VERSION:-latest}
+
+PLATFORM=`uname -s`
+ARCH=`uname -m`
+
+case "$PLATFORM" in
+  CYGWIN*) PLATFORM="Windows"
+  ;;
+  MINGW*) PLATFORM="Windows"
+  ;;
+  MSYS*) PLATFORM="Windows"
+  ;;
+  Darwin) ARCH="universal"
+  ;;
+esac
+
+case "$ARCH" in
+  armv6*) ARCH="armv7"
+  ;;
+  armv7*) ARCH="armv7"
+  ;;
+  armv8*) ARCH="aarch64"
+  ;;
+  armv64*) ARCH="aarch64"
+  ;;
+  aarch64*) ARCH="aarch64"
+  ;;
+esac
+
+# If the install directory is not set, set it to a default
+if [ -z ${INSTALL_DIR+x} ]; then
+  INSTALL_DIR=/usr/local/bin
+fi
+if [ -z ${INSTALL_PATH+x} ]; then
+  INSTALL_PATH="${INSTALL_DIR}/sentry-cli"
+fi
+
+DOWNLOAD_URL="https://release-registry.services.sentry.io/apps/sentry-cli/${VERSION}?response=download&arch=${ARCH}&platform=${PLATFORM}&package=sentry-cli"
+
+echo "This script will automatically install sentry-cli (${VERSION}) for you."
+echo "Installation path: ${INSTALL_PATH}"
+if [ "x$(id -u)" = "x0" ]; then
+  echo "Warning: this script is currently running as root. This is dangerous. "
+  echo "         Instead run it as normal user. We will sudo as needed."
+fi
+
+if [ -f "$INSTALL_PATH" ]; then
+  echo "error: sentry-cli is already installed."
+  echo "  run \"sentry-cli update\" to update to latest version"
+  exit 1
+fi
+
+if ! hash curl 2> /dev/null; then
+  echo "error: you do not have 'curl' installed which is required for this script."
+  exit 1
+fi
+
+TEMP_FILE=`mktemp "${TMPDIR:-/tmp}/.sentrycli.XXXXXXXX"`
+TEMP_HEADER_FILE=`mktemp "${TMPDIR:-/tmp}/.sentrycli-headers.XXXXXXXX"`
+
+cleanup() {
+  rm -f "$TEMP_FILE"
+  rm -f "$TEMP_HEADER_FILE"
+}
+
+trap cleanup EXIT
+HTTP_CODE=$(curl -SL --progress-bar "$DOWNLOAD_URL" -D "$TEMP_HEADER_FILE" --output "$TEMP_FILE" --write-out "%{http_code}")
+if [ ${HTTP_CODE} -lt 200 ] || [ ${HTTP_CODE} -gt 299 ]; then
+  echo "error: your platform and architecture (${PLATFORM}-${ARCH}) is unsupported."
+  exit 1
+fi
+
+for PYTHON in python3 python2 python ''; do
+    if hash "$PYTHON"; then
+        break
+    fi
+done
+
+if [ "$PYTHON" ]; then
+  "$PYTHON" - <<EOF "${TEMP_FILE}" "${TEMP_HEADER_FILE}"
+if 1:
+    import sys
+    import re
+    import hashlib
+    import binascii
+
+    validated = False
+    with open(sys.argv[2], "r") as f:
+        for line in f:
+            match = re.search("(?i)^digest:.?sha256=([^,\n ]+)", line)
+            if match is not None:
+                with open(sys.argv[1], "rb") as downloaded:
+                    hasher = hashlib.sha256()
+                    while True:
+                        chunk = downloaded.read(4096)
+                        if not chunk:
+                            break
+                        hasher.update(chunk)
+                    calculated = hasher.digest()
+                    expected = binascii.a2b_base64(match.group(1))
+                    if calculated != expected:
+                        print("error: checksum mismatch (got %s, expected %s)" % (
+                            binascii.b2a_hex(calculated).decode("ascii"),
+                            binascii.b2a_hex(expected).decode("ascii")
+                        ))
+                        sys.exit(1)
+                    validated = True
+                    break
+    if not validated:
+        print("warning: unable to validate checksum because no checksum available")
+EOF
+else
+  echo "warning: python not available, unable to verify checksums"
+fi
+
+chmod 0755 "$TEMP_FILE"
+if ! (mkdir -p "$(dirname "$INSTALL_PATH")" && mv "$TEMP_FILE" "$INSTALL_PATH") 2> /dev/null; then
+  sudo -k sh -c "mkdir -p \"$(dirname "$INSTALL_PATH")\" && mv \"$TEMP_FILE\" \"$INSTALL_PATH\""
+fi
+
+echo "Sucessfully installed $("$INSTALL_PATH" --version)"
+
+VERSION=$("$INSTALL_PATH" --version | awk '{print $2}')
+MAJOR=$(echo "$VERSION" | cut -d. -f1)
+MINOR=$(echo "$VERSION" | cut -d. -f2)
+if (test -d "${HOME}/.oh-my-zsh") 2>/dev/null && [ $MAJOR -eq 2 ] && [ $MINOR -ge 22 ]; then
+  echo 'Detected Oh My Zsh, installing Zsh completions...'
+  if (mkdir -p "${HOME}/.oh-my-zsh/completions") 2>&1 && ("$INSTALL_PATH" completions zsh > "${HOME}/.oh-my-zsh/completions/_sentry_cli") 2>&1; then
+    echo "Successfully installed Zsh completions."
+  else
+    echo 'Warning: failed to install Zsh completions.'
+  fi
+fi
+
+echo 'Done!'
+"""
+
+
+def get_cli(request: HttpRequest) -> HttpResponse:
+    metrics.incr("cli.download_script")
+    return HttpResponse(SCRIPT, content_type="text/plain")
+
+
+@control_silo_function
+def get_cli_download_url(request: HttpRequest, platform: str, arch: str) -> HttpResponseRedirect:
+    url = "https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch={}&platform={}&package=sentry-cli".format(
+        quote_plus(arch),
+        quote_plus(platform),
+    )
+    return HttpResponseRedirect(url)

+ 8 - 0
src/sentry/web/urls.py

@@ -28,6 +28,7 @@ from sentry.web.frontend.auth_login import AuthLoginView
 from sentry.web.frontend.auth_logout import AuthLogoutView
 from sentry.web.frontend.auth_organization_login import AuthOrganizationLoginView
 from sentry.web.frontend.auth_provider_login import AuthProviderLoginView
+from sentry.web.frontend.cli import get_cli, get_cli_download_url
 from sentry.web.frontend.disabled_member_view import DisabledMemberView
 from sentry.web.frontend.error_page_embed import ErrorPageEmbedView
 from sentry.web.frontend.group_event_json import GroupEventJsonView
@@ -136,6 +137,13 @@ urlpatterns += [
         JavaScriptSdkLoader.as_view(),
         name="sentry-js-sdk-loader",
     ),
+    # docs reference this for acquiring the sentry cli
+    re_path(r"^get-cli/$", get_cli, name="get_cli_script"),
+    re_path(
+        r"^get-cli/(?P<platform>[^/]+)/(?P<arch>[^/]+)/?$",
+        get_cli_download_url,
+        name="get_cli_download_url",
+    ),
     # Versioned API
     re_path(
         r"^api/0/",

+ 20 - 0
tests/sentry/web/frontend/test_cli.py

@@ -0,0 +1,20 @@
+from django.urls import reverse
+
+from sentry.testutils.cases import TestCase
+from sentry.testutils.silo import control_silo_test
+
+
+@control_silo_test
+class GetCliDownloadUrlTestCase(TestCase):
+    def test_cli(self) -> None:
+        resp = self.client.get(reverse("get_cli_script"))
+        assert b"https://release-registry.services.sentry.io/apps/sentry-cli" in resp.content
+
+    def test_valid_platform_arch(self) -> None:
+        resp = self.client.get(reverse("get_cli_download_url", args=("Linux", "x86_64")))
+
+        assert resp.status_code == 302
+        assert (
+            resp["Location"]
+            == "https://release-registry.services.sentry.io/apps/sentry-cli/latest?response=download&arch=x86_64&platform=Linux&package=sentry-cli"
+        )