chore: sync local changes
@@ -0,0 +1,82 @@
|
||||
name: Build and Push to GHCR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["*"]
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: hanzoai/sentry
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Python build dependencies
|
||||
run: |
|
||||
pip install wheel setuptools
|
||||
pip install -r requirements-base.txt
|
||||
|
||||
- name: Build Python wheel
|
||||
run: |
|
||||
python setup.py bdist_wheel
|
||||
mkdir -p dist
|
||||
pip freeze > dist/requirements-frozen.txt
|
||||
|
||||
- name: Build frontend
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn build
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: self-hosted/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
SOURCE_COMMIT=${{ github.sha }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -1,12 +1,12 @@
|
||||
FROM python:3.11.6-slim-bullseye
|
||||
|
||||
LABEL maintainer="oss@sentry.io"
|
||||
LABEL org.opencontainers.image.title="Sentry"
|
||||
LABEL org.opencontainers.image.description="Sentry runtime image"
|
||||
LABEL org.opencontainers.image.url="https://sentry.io/"
|
||||
LABEL org.opencontainers.image.documentation="https://develop.sentry.dev/self-hosted/"
|
||||
LABEL org.opencontainers.image.vendor="Functional Software, Inc."
|
||||
LABEL org.opencontainers.image.authors="oss@sentry.io"
|
||||
LABEL maintainer="ops@hanzo.ai"
|
||||
LABEL org.opencontainers.image.title="Hanzo Sentry"
|
||||
LABEL org.opencontainers.image.description="Hanzo Sentry — error tracking and performance monitoring"
|
||||
LABEL org.opencontainers.image.url="https://sentry.hanzo.ai/"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.hanzo.ai/sentry/"
|
||||
LABEL org.opencontainers.image.vendor="Hanzo AI, Inc."
|
||||
LABEL org.opencontainers.image.authors="ops@hanzo.ai"
|
||||
|
||||
# add our user and group first to make sure their IDs get assigned consistently
|
||||
RUN groupadd -r sentry && useradd -r -m -g sentry sentry
|
||||
@@ -93,5 +93,5 @@ CMD ["run", "web"]
|
||||
ARG SOURCE_COMMIT
|
||||
ENV SENTRY_BUILD=${SOURCE_COMMIT:-unknown}
|
||||
LABEL org.opencontainers.image.revision=$SOURCE_COMMIT
|
||||
LABEL org.opencontainers.image.source="https://github.com/getsentry/sentry/tree/${SOURCE_COMMIT:-master}/"
|
||||
LABEL org.opencontainers.image.licenses="https://github.com/getsentry/sentry/blob/${SOURCE_COMMIT:-master}/LICENSE"
|
||||
LABEL org.opencontainers.image.source="https://github.com/hanzoai/sentry/tree/${SOURCE_COMMIT:-main}/"
|
||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Config(AppConfig):
|
||||
name = "sentry.auth.providers.hanzo"
|
||||
|
||||
def ready(self):
|
||||
from sentry.auth import register
|
||||
|
||||
from .provider import HanzoOAuth2Provider
|
||||
|
||||
register("hanzo", HanzoOAuth2Provider)
|
||||
@@ -0,0 +1,32 @@
|
||||
from sentry.http import safe_urlopen, safe_urlread
|
||||
from sentry.utils import json
|
||||
|
||||
from .constants import USERINFO_URL
|
||||
|
||||
|
||||
class HanzoApiError(Exception):
|
||||
def __init__(self, message="", status=0):
|
||||
self.status = status
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class HanzoClient:
|
||||
def __init__(self, access_token):
|
||||
self.access_token = access_token
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
|
||||
def _request(self, url):
|
||||
headers = {"Authorization": f"Bearer {self.access_token}"}
|
||||
req = safe_urlopen(url, headers=headers)
|
||||
body = safe_urlread(req)
|
||||
if req.status_code != 200:
|
||||
raise HanzoApiError(f"Request failed: {body}", status=req.status_code)
|
||||
return json.loads(body)
|
||||
|
||||
def get_user(self):
|
||||
return self._request(USERINFO_URL)
|
||||
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
# IAM OIDC endpoints (hanzo.id)
|
||||
AUTHORIZE_URL = os.environ.get("SENTRY_OIDC_AUTHORIZE_URL", "https://hanzo.id/oauth/authorize")
|
||||
ACCESS_TOKEN_URL = os.environ.get("SENTRY_OIDC_TOKEN_URL", "https://hanzo.id/oauth/token")
|
||||
USERINFO_URL = os.environ.get("SENTRY_OIDC_USERINFO_URL", "https://hanzo.id/oauth/userinfo")
|
||||
|
||||
CLIENT_ID = os.environ.get("SENTRY_OIDC_CLIENT_ID", "app-sentry")
|
||||
CLIENT_SECRET = os.environ.get("SENTRY_OIDC_CLIENT_SECRET", "")
|
||||
|
||||
SCOPE = "openid profile email"
|
||||
@@ -0,0 +1,62 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from sentry.auth.exceptions import IdentityNotValid
|
||||
from sentry.auth.providers.oauth2 import OAuth2Callback, OAuth2Login, OAuth2Provider
|
||||
|
||||
from .client import HanzoApiError, HanzoClient
|
||||
from .constants import ACCESS_TOKEN_URL, AUTHORIZE_URL, CLIENT_ID, CLIENT_SECRET, SCOPE
|
||||
from .views import FetchUser
|
||||
|
||||
|
||||
class HanzoOAuth2Provider(OAuth2Provider):
|
||||
access_token_url = ACCESS_TOKEN_URL
|
||||
authorize_url = AUTHORIZE_URL
|
||||
name = "Hanzo"
|
||||
|
||||
def get_client_id(self):
|
||||
return CLIENT_ID
|
||||
|
||||
def get_client_secret(self):
|
||||
return CLIENT_SECRET
|
||||
|
||||
def get_auth_pipeline(self):
|
||||
return [
|
||||
OAuth2Login(
|
||||
authorize_url=self.authorize_url,
|
||||
client_id=self.get_client_id(),
|
||||
scope=SCOPE,
|
||||
),
|
||||
OAuth2Callback(
|
||||
access_token_url=self.access_token_url,
|
||||
client_id=self.get_client_id(),
|
||||
client_secret=self.get_client_secret(),
|
||||
),
|
||||
FetchUser(),
|
||||
]
|
||||
|
||||
def get_setup_pipeline(self):
|
||||
return self.get_auth_pipeline()
|
||||
|
||||
def get_refresh_token_url(self):
|
||||
return ACCESS_TOKEN_URL
|
||||
|
||||
def build_config(self, state):
|
||||
return {}
|
||||
|
||||
def build_identity(self, state: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
data = state["data"]
|
||||
user_data = state["user"]
|
||||
return {
|
||||
"id": user_data.get("sub") or user_data.get("id", ""),
|
||||
"email": user_data.get("email", ""),
|
||||
"name": user_data.get("name", user_data.get("displayName", "")),
|
||||
"data": self.get_oauth_data(data),
|
||||
}
|
||||
|
||||
def refresh_identity(self, auth_identity):
|
||||
with HanzoClient(auth_identity.data["access_token"]) as client:
|
||||
try:
|
||||
client.get_user()
|
||||
except HanzoApiError as e:
|
||||
raise IdentityNotValid(e)
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.http import HttpResponse
|
||||
from rest_framework.request import Request
|
||||
|
||||
from sentry.auth.view import AuthView
|
||||
|
||||
from .client import HanzoClient
|
||||
|
||||
|
||||
class FetchUser(AuthView):
|
||||
def dispatch(self, request: Request, helper) -> HttpResponse:
|
||||
access_token = helper.fetch_state("data")["access_token"]
|
||||
with HanzoClient(access_token) as client:
|
||||
user = client.get_user()
|
||||
|
||||
helper.bind_state("user", user)
|
||||
return helper.next_step()
|
||||
@@ -409,6 +409,7 @@ INSTALLED_APPS: tuple[str, ...] = (
|
||||
"sentry.eventstream",
|
||||
"sentry.auth.providers.google.apps.Config",
|
||||
"sentry.auth.providers.fly.apps.Config",
|
||||
"sentry.auth.providers.hanzo.apps.Config",
|
||||
"django.contrib.staticfiles",
|
||||
"sentry.issues.apps.Config",
|
||||
"sentry.feedback",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="64" height="64" rx="8" fill="#000000"/>
|
||||
<g transform="translate(8, 8) scale(0.716)">
|
||||
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
|
||||
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
|
||||
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
|
||||
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
|
||||
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
|
After Width: | Height: | Size: 366 B |
@@ -1 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><path d="M79 72a7.3 7.3 0 0 0 0-7.5L46.4 8A7.2 7.2 0 0 0 40 4.4 7.3 7.3 0 0 0 33.6 8L23.1 26.3l2.6 1.6a51.4 51.4 0 0 1 18.9 18.9 50.4 50.4 0 0 1 6.8 22.7H44a44.3 44.3 0 0 0-5.8-19A43.8 43.8 0 0 0 22 34.2l-2.7-1.5-9.8 17 2.6 1.6a24.9 24.9 0 0 1 12.1 18.2h-17a1.2 1.2 0 0 1-1-.6 1.2 1.2 0 0 1 0-1.3l4.8-8a17.7 17.7 0 0 0-5.4-3.2L1 64.6A7.3 7.3 0 0 0 1 72a7.3 7.3 0 0 0 6.4 3.7h23.2v-3.2a31.3 31.3 0 0 0-4-15.3 31.3 31.3 0 0 0-8.7-9.6l3.7-6.4a38 38 0 0 1 11.3 12.3 38.1 38.1 0 0 1 5.1 19v3h19.7v-3a57.5 57.5 0 0 0-7.8-28.8A57.5 57.5 0 0 0 31.5 24l7.5-13a1.2 1.2 0 0 1 1-.6 1.2 1.2 0 0 1 1 .6l32.7 56.5a1.2 1.2 0 0 1 0 1.2 1.1 1.1 0 0 1-1 .6H65v6.2h7.7a7.1 7.1 0 0 0 6.3-3.7z"/></svg>
|
||||
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
|
||||
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#DDDDDD"/>
|
||||
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
|
||||
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
|
||||
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
|
||||
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#DDDDDD"/>
|
||||
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 741 B After Width: | Height: | Size: 576 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 67 67" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.21 67V44.6369H0V67H22.21Z" fill="#ffffff"/>
|
||||
<path d="M0 44.6369L22.21 46.8285V44.6369H0Z" fill="#DDDDDD"/>
|
||||
<path d="M66.7038 22.3184H22.2534L0.0878906 44.6367H44.4634L66.7038 22.3184Z" fill="#ffffff"/>
|
||||
<path d="M22.21 0H0V22.3184H22.21V0Z" fill="#ffffff"/>
|
||||
<path d="M66.7198 0H44.5098V22.3184H66.7198V0Z" fill="#ffffff"/>
|
||||
<path d="M66.6753 22.3185L44.5098 20.0822V22.3185H66.6753Z" fill="#DDDDDD"/>
|
||||
<path d="M66.7198 67V44.6369H44.5098V67H66.7198Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 576 B |