diff --git a/.github/workflows/deploy_gcp_admin_app.yaml b/.github/workflows/deploy_gcp_admin_app.yaml new file mode 100644 index 000000000..b47ebf3ff --- /dev/null +++ b/.github/workflows/deploy_gcp_admin_app.yaml @@ -0,0 +1,103 @@ +name: Deploy admin_app to GCP + +on: + push: + branches: + - main + - testing + - production + paths: + - "admin_app/**" + - ".github/workflows/deploy_gcp_admin_app.yaml" + workflow_dispatch: + +jobs: + DeployAdminAppToGCP: + runs-on: ubuntu-latest + + permissions: + contents: "read" + id-token: "write" + + # TODO: replace improve-gcp-deploy with main + environment: gcp-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + env: + RESOURCE_PREFIX: ${{ secrets.PROJECT_NAME }}-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + REPO: ${{ secrets.DOCKER_REGISTRY_DOMAIN }}/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.PROJECT_NAME }}-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + name: "Authenticate to Google Cloud" + uses: "google-github-actions/auth@v2" + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ vars.POOL_ID }}/providers/${{ vars.PROVIDER_ID }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} + + - name: Retrieve secrets from Secret Manager + id: "secrets" + uses: "google-github-actions/get-secretmanager-secrets@v2" + with: + min_mask_length: 4 + secrets: |- + domain:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-domain + google_login_client_id:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-google-login-client-id + + - name: Configure Docker to use gcloud as a credential helper + run: | + gcloud auth configure-docker ${{ secrets.DOCKER_REGISTRY_DOMAIN}} + + - name: Build and push admin_app image + working-directory: admin_app + run: | + docker build \ + --build-arg NEXT_PUBLIC_BACKEND_URL="https://${{ steps.secrets.outputs.domain }}/api" \ + --build-arg NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID="${{ steps.secrets.outputs.google_login_client_id }}" \ + -t ${{ env.REPO }}/admin_app:latest \ + -t ${{ env.REPO }}/admin_app:${{ github.sha }} \ + . + docker image push --all-tags ${{ env.REPO }}/admin_app + + - name: Deploy admin_app container + id: "compute-ssh" + uses: "google-github-actions/ssh-compute@v1" + env: + REPO: ${{ secrets.DOCKER_REGISTRY_DOMAIN }}/${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }} + with: + instance_name: "${{ secrets.DEPLOYMENT_INSTANCE_NAME }}" + zone: "${{ secrets.DEPLOYMENT_ZONE }}" + ssh_private_key: "${{ secrets.GCP_SSH_PRIVATE_KEY }}" + command: | + docker-credential-gcr configure-docker \ + --registries ${{ secrets.DOCKER_REGISTRY_DOMAIN }} + docker pull \ + ${{ env.REPO }}/admin_app:latest + docker stop admin_app + docker rm admin_app + docker run -d \ + --log-driver=gcplogs \ + --restart always \ + --network aaq-network \ + --name admin_app \ + ${{ env.REPO }}/admin_app:latest + docker system prune -f + + - name: Show deployment command output + run: |- + echo '${{ steps.compute-ssh.outputs.stdout }}' + echo '${{ steps.compute-ssh.outputs.stderr }}' + + - name: Wait for Application to start + id: wait-for-app + run: sleep 1m + shell: bash + + - name: Check if deployment was successful + id: check-deployment + run: | + curl -f -X 'GET' \ + 'https://${{ steps.secrets.outputs.domain }}/api/healthcheck' \ + -H 'accept: application/json' diff --git a/.github/workflows/deploy_gcp_caddy.yaml b/.github/workflows/deploy_gcp_caddy.yaml new file mode 100644 index 000000000..7b8cfd008 --- /dev/null +++ b/.github/workflows/deploy_gcp_caddy.yaml @@ -0,0 +1,93 @@ +name: Deploy Caddy to GCP + +on: + push: + branches: + - main + - testing + - production + paths: + - "deployment/docker-compose/caddy/**" + - ".github/workflows/deploy_gcp_caddy.yaml" + workflow_dispatch: + +jobs: + DeployCaddyToGCP: + runs-on: ubuntu-latest + + permissions: + contents: "read" + id-token: "write" + + # TODO: replace improve-gcp-deploy with main + environment: gcp-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + env: + RESOURCE_PREFIX: ${{ secrets.PROJECT_NAME }}-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + name: "Authenticate to Google Cloud" + uses: "google-github-actions/auth@v2" + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ vars.POOL_ID }}/providers/${{ vars.PROVIDER_ID }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} + + - name: Retrieve secrets from Secret Manager + id: "secrets" + uses: "google-github-actions/get-secretmanager-secrets@v2" + with: + secrets: |- + domain:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-domain + + - name: Copy Caddy deployment files + working-directory: deployment/docker-compose + run: | + gcloud compute scp Caddyfile \ + ${{ secrets.DEPLOYMENT_INSTANCE_NAME }}:~/Caddyfile \ + --zone ${{ secrets.DEPLOYMENT_ZONE }} + + - name: Deploy Caddy container + id: "compute-ssh" + uses: "google-github-actions/ssh-compute@v1" + with: + instance_name: "${{ secrets.DEPLOYMENT_INSTANCE_NAME }}" + zone: "${{ secrets.DEPLOYMENT_ZONE }}" + ssh_private_key: "${{ secrets.GCP_SSH_PRIVATE_KEY }}" + command: | + docker stop caddy + docker rm caddy + docker run -d \ + -v caddy_data:/data \ + -v caddy_config:/config \ + -e DOMAIN=${{ steps.secrets.outputs.domain }} \ + -p 80:80 \ + -p 443:443 \ + -p 443:443/udp \ + -v ~/Caddyfile:/etc/caddy/Caddyfile \ + --log-driver=gcplogs \ + --restart always \ + --network aaq-network \ + --name caddy \ + caddy:2.7.6 + docker system prune --volumes -f + + - name: Show deployment command output + run: |- + echo '${{ steps.compute-ssh.outputs.stdout }}' + echo '${{ steps.compute-ssh.outputs.stderr }}' + + - name: Wait for Application to start + id: wait-for-app + run: sleep 1m + shell: bash + + - name: Check if deployment was successful + id: check-deployment + run: | + curl -f -X 'GET' \ + 'https://${{ steps.secrets.outputs.domain }}/api/healthcheck' \ + -H 'accept: application/json' diff --git a/.github/workflows/deploy_gcp_core_backend.yaml b/.github/workflows/deploy_gcp_core_backend.yaml new file mode 100644 index 000000000..ef91ed722 --- /dev/null +++ b/.github/workflows/deploy_gcp_core_backend.yaml @@ -0,0 +1,122 @@ +name: Deploy core_backend to GCP + +on: + push: + branches: + - main + - testing + - production + paths: + - "core_backend/**" + - ".github/workflows/deploy_gcp_core_backend.yaml" + workflow_dispatch: + +jobs: + DeployCoreBackendToGCP: + runs-on: ubuntu-latest + + permissions: + contents: "read" + id-token: "write" + + # TODO: replace improve-gcp-deploy with main + environment: gcp-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + env: + RESOURCE_PREFIX: ${{ secrets.PROJECT_NAME }}-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + REPO: ${{ secrets.DOCKER_REGISTRY_DOMAIN }}/${{ secrets.GCP_PROJECT_ID }}/${{ secrets.PROJECT_NAME }}-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + name: "Authenticate to Google Cloud" + uses: "google-github-actions/auth@v2" + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ vars.POOL_ID }}/providers/${{ vars.PROVIDER_ID }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} + + - name: Retrieve secrets from Secret Manager + id: "secrets" + uses: "google-github-actions/get-secretmanager-secrets@v2" + with: + secrets: |- + domain:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-domain + jwt-secret:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-jwt-secret + google-login-client-id:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-google-login-client-id + langfuse-secret-key:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-langfuse-secret-key + langfuse-public-key:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-langfuse-public-key + db-host:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-db-host + db-password:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-db-password + admin-username:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-admin-username + admin-password:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-admin-password + admin-api-key:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-admin-api-key + + - name: Configure Docker to use gcloud as a credential helper + run: | + gcloud auth configure-docker ${{ secrets.DOCKER_REGISTRY_DOMAIN}} + + - name: Build and push core_backend image + working-directory: core_backend + run: | + docker build \ + -t ${{ env.REPO }}/core_backend:latest \ + -t ${{ env.REPO }}/core_backend:${{ github.sha }} \ + . + docker image push --all-tags ${{ env.REPO }}/core_backend + + - name: Deploy core_backend container + id: "compute-ssh" + uses: "google-github-actions/ssh-compute@v1" + env: + REPO: ${{ secrets.DOCKER_REGISTRY_DOMAIN }}/${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }} + with: + instance_name: "${{ secrets.DEPLOYMENT_INSTANCE_NAME }}" + zone: "${{ secrets.DEPLOYMENT_ZONE }}" + ssh_private_key: "${{ secrets.GCP_SSH_PRIVATE_KEY }}" + command: | + docker-credential-gcr configure-docker \ + --registries ${{ secrets.DOCKER_REGISTRY_DOMAIN }} + docker pull \ + ${{ env.REPO }}/core_backend:latest + docker stop core_backend + docker rm core_backend + docker run -d \ + --log-driver=gcplogs \ + --restart always \ + --network aaq-network \ + --name core_backend \ + -e JWT_SECRET="${{ steps.secrets.outputs.jwt-secret }}" \ + -e NEXT_PUBLIC_GOOGLE_LOGIN_CLIENT_ID="${{ steps.secrets.outputs.google-login-client-id }}" \ + -e DOMAIN="${{ steps.secrets.outputs.domain }}" \ + -e POSTGRES_HOST="${{ steps.secrets.outputs.db-host }}" \ + -e POSTGRES_PASSWORD="${{ steps.secrets.outputs.db-password }}" \ + -e ADMIN_USERNAME="${{ steps.secrets.outputs.admin-username }}" \ + -e ADMIN_PASSWORD="${{ steps.secrets.outputs.admin-password }}" \ + -e ADMIN_API_KEY="${{ steps.secrets.outputs.admin-api-key }}" \ + -e PROMETHEUS_MULTIPROC_DIR=/tmp \ + -e LITELLM_ENDPOINT=http://litellm_proxy:4000 \ + -e LANGFUSE=True \ + -e LANGFUSE_SECRET_KEY="${{ steps.secrets.outputs.langfuse-secret-key }}" \ + -e LANGFUSE_PUBLIC_KEY="${{ steps.secrets.outputs.langfuse-public-key }}" \ + -e BACKEND_ROOT_PATH=/api \ + ${{ env.REPO }}/core_backend:latest + docker system prune -f + + - name: Show deployment command output + run: |- + echo '${{ steps.compute-ssh.outputs.stdout }}' + echo '${{ steps.compute-ssh.outputs.stderr }}' + + - name: Wait for Application to start + id: wait-for-app + run: sleep 1m + shell: bash + + - name: Check if deployment was successful + id: check-deployment + run: | + curl -f -X 'GET' \ + 'https://${{ steps.secrets.outputs.domain }}/api/healthcheck' \ + -H 'accept: application/json' diff --git a/.github/workflows/deploy_gcp_litellm_proxy.yaml b/.github/workflows/deploy_gcp_litellm_proxy.yaml new file mode 100644 index 000000000..5470e37ed --- /dev/null +++ b/.github/workflows/deploy_gcp_litellm_proxy.yaml @@ -0,0 +1,89 @@ +name: Deploy LiteLLM Proxy to GCP + +on: + push: + branches: + - main + - testing + - production + paths: + - "deployment/docker-compose/litellm_proxy/**" + - ".github/workflows/deploy_gcp_litellm_proxy.yaml" + workflow_dispatch: + +jobs: + DeployLiteLLMProxyToGCP: + runs-on: ubuntu-latest + + permissions: + contents: "read" + id-token: "write" + + # TODO: replace improve-gcp-deploy with main + environment: gcp-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + env: + RESOURCE_PREFIX: ${{ secrets.PROJECT_NAME }}-${{ (github.ref_name == 'main' && 'testing') || github.ref_name }} + + steps: + - uses: "actions/checkout@v4" + + - id: "auth" + name: "Authenticate to Google Cloud" + uses: "google-github-actions/auth@v2" + with: + project_id: ${{ secrets.GCP_PROJECT_ID }} + workload_identity_provider: projects/${{ secrets.GCP_PROJECT_NUMBER }}/locations/global/workloadIdentityPools/${{ vars.POOL_ID }}/providers/${{ vars.PROVIDER_ID }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT_EMAIL }} + + - name: Retrieve secrets from Secret Manager + id: "secrets" + uses: "google-github-actions/get-secretmanager-secrets@v2" + with: + secrets: |- + domain:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-domain + openai-api-key:${{ secrets.GCP_PROJECT_ID }}/${{ env.RESOURCE_PREFIX }}-openai-api-key + + - name: Copy LiteLLM deployment files + working-directory: deployment/docker-compose + run: | + gcloud compute scp litellm_proxy_config.yaml \ + ${{ secrets.DEPLOYMENT_INSTANCE_NAME }}:~/litellm_proxy_config.yaml \ + --zone ${{ secrets.DEPLOYMENT_ZONE }} + + - name: Deploy LiteLLM Proxy container + id: "compute-ssh" + uses: "google-github-actions/ssh-compute@v1" + with: + instance_name: "${{ secrets.DEPLOYMENT_INSTANCE_NAME }}" + zone: "${{ secrets.DEPLOYMENT_ZONE }}" + ssh_private_key: "${{ secrets.GCP_SSH_PRIVATE_KEY }}" + command: | + docker stop litellm_proxy + docker rm litellm_proxy + docker run -d \ + -v ~/litellm_proxy_config.yaml:/app/config.yaml \ + -e OPENAI_API_KEY="${{ steps.secrets.outputs.openai-api-key }}" \ + --log-driver=gcplogs \ + --restart always \ + --network aaq-network \ + --name litellm_proxy \ + ghcr.io/berriai/litellm:main-v1.34.6 --config /app/config.yaml + docker system prune -f + + - name: Show deployment command output + run: |- + echo '${{ steps.compute-ssh.outputs.stdout }}' + echo '${{ steps.compute-ssh.outputs.stderr }}' + + - name: Wait for Application to start + id: wait-for-app + run: sleep 1m + shell: bash + + - name: Check if deployment was successful + id: check-deployment + run: | + curl -f -X 'GET' \ + 'https://${{ steps.secrets.outputs.domain }}/api/healthcheck' \ + -H 'accept: application/json' diff --git a/Makefile b/Makefile index 4e59076d7..68be0bb87 100644 --- a/Makefile +++ b/Makefile @@ -74,7 +74,6 @@ setup-llm-proxy: -d ghcr.io/berriai/litellm:main-v1.34.6 \ --config /app/config.yaml --detailed_debug - teardown-llm-proxy: @docker stop litellm-proxy @docker rm litellm-proxy diff --git a/admin_app/src/app/dashboard/layout.tsx b/admin_app/src/app/dashboard/layout.tsx index d71d3473e..dc2524687 100644 --- a/admin_app/src/app/dashboard/layout.tsx +++ b/admin_app/src/app/dashboard/layout.tsx @@ -2,6 +2,7 @@ import NavBar from "@/components/NavBar"; import { ProtectedComponent } from "@/components/ProtectedComponent"; import React from "react"; + export default function RootLayout({ children, }: Readonly<{ diff --git a/core_backend/Makefile b/core_backend/Makefile index 0c0bdb590..47783d82f 100644 --- a/core_backend/Makefile +++ b/core_backend/Makefile @@ -33,7 +33,7 @@ teardown-test-db: @docker stop testdb @docker rm testdb -# alignscore containers (if needed) +# alignscore containers (if it is needed) setup-alignscore-container: -@docker stop testalignscore -@docker rm testalignscore diff --git a/core_backend/app/database.py b/core_backend/app/database.py index e54c0bee5..fe89a3df4 100644 --- a/core_backend/app/database.py +++ b/core_backend/app/database.py @@ -1,9 +1,9 @@ import contextlib import os from collections.abc import AsyncGenerator, Generator -from typing import ContextManager +from typing import ContextManager, Union -from sqlalchemy.engine import Engine, create_engine +from sqlalchemy.engine import URL, Engine, create_engine from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine from sqlalchemy.orm import Session @@ -29,24 +29,32 @@ _ASYNC_ENGINE: AsyncEngine | None = None -def build_connection_string( +def get_connection_url( *, db_api: str = ASYNC_DB_API, user: str = POSTGRES_USER, password: str = POSTGRES_PASSWORD, host: str = POSTGRES_HOST, - port: str = POSTGRES_PORT, + port: Union[int, str] = POSTGRES_PORT, db: str = POSTGRES_DB, -) -> str: + render_as_string: bool = False, +) -> URL: """Return a connection string for the given database.""" - return f"postgresql+{db_api}://{user}:{password}@{host}:{port}/{db}" + return URL.create( + drivername="postgresql+" + db_api, + username=user, + host=host, + password=password, + port=int(port), + database=db, + ) def get_sqlalchemy_engine() -> Engine: """Return a SQLAlchemy engine.""" global _SYNC_ENGINE if _SYNC_ENGINE is None: - connection_string = build_connection_string(db_api=SYNC_DB_API) + connection_string = get_connection_url(db_api=SYNC_DB_API) _SYNC_ENGINE = create_engine(connection_string) return _SYNC_ENGINE @@ -55,7 +63,7 @@ def get_sqlalchemy_async_engine() -> AsyncEngine: """Return a SQLAlchemy async engine generator.""" global _ASYNC_ENGINE if _ASYNC_ENGINE is None: - connection_string = build_connection_string() + connection_string = get_connection_url() _ASYNC_ENGINE = create_async_engine(connection_string, pool_size=DB_POOL_SIZE) return _ASYNC_ENGINE diff --git a/core_backend/migrations/env.py b/core_backend/migrations/env.py index 7028f836a..2123da325 100644 --- a/core_backend/migrations/env.py +++ b/core_backend/migrations/env.py @@ -2,7 +2,7 @@ from alembic import context from app import models -from app.database import SYNC_DB_API, build_connection_string +from app.database import SYNC_DB_API, get_connection_url from sqlalchemy import engine_from_config, pool # this is the Alembic Config object, which provides @@ -10,7 +10,10 @@ config = context.config # this will overwrite the ini-file sqlalchemy.url path -config.set_main_option("sqlalchemy.url", build_connection_string(db_api=SYNC_DB_API)) +connection_url = get_connection_url(db_api=SYNC_DB_API) +connection_string = connection_url.render_as_string(hide_password=False) +# Don't use '%' in password: https://stackoverflow.com/a/40837579/25741288 +config.set_main_option("sqlalchemy.url", connection_string) # Interpret the config file for Python logging. # This line sets up loggers basically. diff --git a/core_backend/tests/api/conftest.py b/core_backend/tests/api/conftest.py index 76bf53701..5a0d5a55c 100644 --- a/core_backend/tests/api/conftest.py +++ b/core_backend/tests/api/conftest.py @@ -21,7 +21,7 @@ ) from core_backend.app.contents.config import PGVECTOR_VECTOR_SIZE from core_backend.app.contents.models import ContentDB -from core_backend.app.database import build_connection_string, get_session +from core_backend.app.database import get_connection_url, get_session from core_backend.app.llm_call import process_input, process_output from core_backend.app.llm_call.llm_prompts import ( RAG, @@ -84,7 +84,7 @@ def db_session() -> Generator[Session, None, None]: # See https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html#using-multiple-asyncio-event-loops @pytest.fixture(scope="function") async def async_engine() -> AsyncGenerator[AsyncEngine, None]: - connection_string = build_connection_string() + connection_string = get_connection_url() engine = create_async_engine(connection_string, pool_size=20) yield engine await engine.dispose()