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

feat: Improve Git handler and introduce caching #1689

Merged
merged 5 commits into from
Sep 20, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ certs/*
/helm/charts/*
/logs/*
.env
autoscaler
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ create-cluster: registry
kubectl cluster-info
kubectl config set-context --current --namespace=$(NAMESPACE)

install-vpa:
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh
kubectl --namespace=kube-system get pods | grep vpa

delete-cluster:
k3d cluster list $(CLUSTER_NAME) 2>&- && k3d cluster delete $(CLUSTER_NAME)

Expand Down
19 changes: 18 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ DB_PORT = 5432
DB_PASSWORD = dev
DB_USER = dev
DB_NAME = dev

VALKEY_PASSWORD ?= password

VENV = .venv

HOST ?= 127.0.0.1

VALKEY_PORT = 6379

DATABASE_LOAD_FILE ?= ../local/load.sql
DATABASE_SAVE_DIR ?= ../local

Expand All @@ -31,6 +36,18 @@ database:
-e POSTGRES_DB=$(DB_NAME) \
postgres

valkey:
TMP_FILE=$$(mktemp)
echo "requirepass $(VALKEY_PASSWORD)" > $$TMP_FILE
docker start capella-collab-valkey || \
docker run -d \
--name capella-collab-valkey \
-p $(VALKEY_PORT):6379 \
-v $$TMP_FILE:/usr/local/etc/valkey/valkey.conf \
valkey/valkey:latest \
valkey-server \
/usr/local/etc/valkey/valkey.conf

app:
if [ -d "$(VENV)/bin" ]; then
source $(VENV)/bin/activate;
Expand All @@ -57,7 +74,7 @@ install:
openapi:
CAPELLACOLLAB_SKIP_OPENAPI_ERROR_RESPONSES=1 $(VENV)/bin/python -m capellacollab.cli openapi generate /tmp/openapi.json

dev: database app
dev: database valkey app

cleanup:
docker stop capella-collab-postgres
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

"""Add repository id to git model and remove unused git model name

Revision ID: abddaf015966
Revises: 028c72ddfd20
Create Date: 2024-08-12 11:43:34.158404

"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "abddaf015966"
down_revision = "028c72ddfd20"
branch_labels = None
depends_on = None


def upgrade():
op.add_column(
"git_models", sa.Column("repository_id", sa.String(), nullable=True)
)
op.drop_column("git_models", "name")
7 changes: 7 additions & 0 deletions backend/capellacollab/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,12 @@ class DatabaseConfig(BaseConfig):
)


class ValkeyConfig(BaseConfig):
url: str = pydantic.Field(
default="valkey://default:password@localhost:6379/0"
)


class InitialConfig(BaseConfig):
admin: str = pydantic.Field(
default="admin",
Expand Down Expand Up @@ -367,6 +373,7 @@ class AppConfig(BaseConfig):
authentication: AuthenticationConfig = AuthenticationConfig()
prometheus: PrometheusConfig = PrometheusConfig()
database: DatabaseConfig = DatabaseConfig()
valkey: ValkeyConfig = ValkeyConfig()
initial: InitialConfig = InitialConfig()
logging: LoggingConfig = LoggingConfig()
requests: RequestsConfig = RequestsConfig()
Expand Down
7 changes: 7 additions & 0 deletions backend/capellacollab/core/database/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import functools
import typing as t

import pydantic
import sqlalchemy as sa
import valkey
from sqlalchemy import orm
from sqlalchemy.dialects import postgresql

Expand Down Expand Up @@ -35,6 +37,11 @@ def get_db() -> t.Iterator[orm.Session]:
yield session


@functools.lru_cache
def get_valkey() -> valkey.Valkey:
return valkey.Valkey.from_url(config.valkey.url, decode_responses=True)


def patch_database_with_pydantic_object(
database_object: Base, pydantic_object: pydantic.BaseModel
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class DiagramMetadata(core_pydantic.BaseModel):
class DiagramCacheMetadata(core_pydantic.BaseModel):
diagrams: list[DiagramMetadata]
last_updated: datetime.datetime
job_id: str | None = None

_validate_last_updated = pydantic.field_serializer("last_updated")(
core_pydantic.datetime_serializer
Expand Down
53 changes: 34 additions & 19 deletions backend/capellacollab/projects/toolmodels/diagrams/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import json
import logging
import pathlib
from urllib import parse
Expand Down Expand Up @@ -41,24 +42,30 @@
logger: logging.LoggerAdapter = fastapi.Depends(log.get_request_logger),
):
try:
(
last_updated,
diagram_metadata_entries,
) = await handler.get_file_from_repository_or_artifacts_as_json(
"diagram_cache/index.json",
"update_capella_diagram_cache",
"diagram-cache/" + handler.git_model.revision,
job_id, last_updated, diagram_metadata_entries = (
await handler.get_file_or_artifact(
trusted_file_path="diagram_cache/index.json",
logger=logger,
job_name="update_capella_diagram_cache",
file_revision=f"diagram-cache/{handler.revision}",
)
)
except requests.HTTPError:
logger.info(

Check warning on line 54 in backend/capellacollab/projects/toolmodels/diagrams/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/diagrams/routes.py#L54

Added line #L54 was not covered by tests
"Failed fetching diagram metadata file or artifact for %s",
handler.path,
exc_info=True,
)
except requests.exceptions.HTTPError:
logger.info("Failed fetching diagram metadata", exc_info=True)
raise exceptions.DiagramCacheNotConfiguredProperlyError()

diagram_metadata_entries = json.loads(diagram_metadata_entries)
return models.DiagramCacheMetadata(
diagrams=[
models.DiagramMetadata.model_validate(diagram_metadata)
for diagram_metadata in diagram_metadata_entries
],
last_updated=last_updated,
job_id=job_id,
)


Expand All @@ -69,6 +76,7 @@
)
async def get_diagram(
diagram_uuid_or_filename: str,
job_id: str | None = None,
handler: git_handler.GitHandler = fastapi.Depends(
git_injectables.get_git_handler
),
Expand All @@ -79,16 +87,23 @@
raise exceptions.FileExtensionNotSupportedError(fileextension)

diagram_uuid = pathlib.PurePosixPath(diagram_uuid_or_filename).stem
file_path = f"diagram_cache/{parse.quote(diagram_uuid, safe='')}.svg"

try:
_, diagram = await handler.get_file_from_repository_or_artifacts(
f"diagram_cache/{parse.quote(diagram_uuid, safe='')}.svg",
"update_capella_diagram_cache",
"diagram-cache/" + handler.git_model.revision,
file_or_artifact = await handler.get_file_or_artifact(
trusted_file_path=file_path,
logger=logger,
job_name="update_capella_diagram_cache",
job_id=job_id,
file_revision=f"diagram-cache/{handler.revision}",
)
return responses.SVGResponse(content=file_or_artifact[2])
except requests.HTTPError:
logger.info(

Check warning on line 102 in backend/capellacollab/projects/toolmodels/diagrams/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/diagrams/routes.py#L101-L102

Added lines #L101 - L102 were not covered by tests
"Failed fetching diagram file or artifact %s for %s.",
diagram_uuid,
handler.path,
f"diagram-cache/{handler.revision}",
exc_info=True,
)
except requests.exceptions.HTTPError:
logger.info("Failed fetching diagram", exc_info=True)
raise exceptions.DiagramCacheNotConfiguredProperlyError()

return responses.SVGResponse(
content=diagram,
)
22 changes: 12 additions & 10 deletions backend/capellacollab/projects/toolmodels/modelbadge/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

import logging

import aiohttp.web
import fastapi
import requests

import capellacollab.projects.toolmodels.modelsources.git.injectables as git_injectables
from capellacollab.core import logging as log
Expand Down Expand Up @@ -41,13 +39,17 @@
logger: logging.LoggerAdapter = fastapi.Depends(log.get_request_logger),
):
try:
return responses.SVGResponse(
content=(
await git_handler.get_file_from_repository_or_artifacts(
"model-complexity-badge.svg", "generate-model-badge"
)
)[1],
file_or_artifact = await git_handler.get_file_or_artifact(
trusted_file_path="model-complexity-badge.svg",
job_name="generate-model-badge",
logger=logger,
)
return responses.SVGResponse(content=file_or_artifact[2])
except Exception:
logger.debug(

Check warning on line 49 in backend/capellacollab/projects/toolmodels/modelbadge/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/modelbadge/routes.py#L48-L49

Added lines #L48 - L49 were not covered by tests
"Failed fetching model badge file or artifact for %s on revision %s.",
git_handler.path,
git_handler.revision,
exc_info=True,
)
except (aiohttp.web.HTTPException, requests.exceptions.HTTPError):
logger.info("Failed fetching model complexity badge", exc_info=True)
raise exceptions.ModelBadgeNotConfiguredProperlyError()
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,36 @@
def update_git_model(
db: orm.Session,
git_model: models.DatabaseGitModel,
patch_model: models.PatchGitModel,
put_model: models.PutGitModel,
) -> models.DatabaseGitModel:
git_model.path = patch_model.path
git_model.entrypoint = patch_model.entrypoint
git_model.revision = patch_model.revision

if patch_model.password:
git_model.username = patch_model.username
git_model.password = patch_model.password
elif not patch_model.username:
git_model.path = put_model.path
git_model.entrypoint = put_model.entrypoint
git_model.revision = put_model.revision
git_model.repository_id = None

if put_model.password:
git_model.username = put_model.username
git_model.password = put_model.password

Check warning on line 73 in backend/capellacollab/projects/toolmodels/modelsources/git/crud.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/modelsources/git/crud.py#L72-L73

Added lines #L72 - L73 were not covered by tests
elif not put_model.username:
git_model.username = ""
git_model.password = ""

if patch_model.primary and not git_model.primary:
if put_model.primary and not git_model.primary:
git_model = make_git_model_primary(db, git_model)

db.commit()
return git_model


def update_git_model_repository_id(
MoritzWeber0 marked this conversation as resolved.
Show resolved Hide resolved
db: orm.Session, git_model: models.DatabaseGitModel, repository_id: str
) -> models.DatabaseGitModel:
git_model.repository_id = repository_id

db.commit()
return git_model


def delete_git_model(db: orm.Session, git_model: models.DatabaseGitModel):
db.delete(git_model)
db.commit()
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,6 @@ def __init__(self, filename: str):
)


class GitInstanceAPIEndpointNotFoundError(core_exceptions.BaseError):
def __init__(self):
super().__init__(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
title="Git instance API endpoint not found",
reason=(
"The used Git instance has no API endpoint defined. "
"Please contact your administrator."
),
err_code="GIT_INSTANCE_NO_API_ENDPOINT_DEFINED",
)


class GitPipelineJobNotFoundError(core_exceptions.BaseError):
def __init__(self, job_name: str, revision: str):
super().__init__(
Expand All @@ -97,31 +84,18 @@ def __init__(self, job_name: str, revision: str):
)


class GitPipelineJobFailedError(core_exceptions.BaseError):
def __init__(self, job_name: str):
super().__init__(
status_code=status.HTTP_400_BAD_REQUEST,
title="Failed job found",
reason=f"The last job with the name '{job_name}' has failed.",
err_code="FAILED_JOB_FOUND",
)


class GitPipelineJobUnknownStateError(core_exceptions.BaseError):
job_name: str
state: str

class GitPipelineJobUnsuccessfulError(core_exceptions.BaseError):
def __init__(self, job_name: str, state: str):
self.job_name = job_name
self.state = state
super().__init__(
status_code=status.HTTP_400_BAD_REQUEST,
title="Unknown job state",
title="Unsuccessful job",
reason=(
f"Job '{job_name}' has an unhandled or unknown state: '{state}'. "
f"Job '{job_name}' has an unsuccessful state: {self.state}."
"Please contact your administrator."
),
err_code="UNKNOWN_STATE_ERROR",
err_code="UNSUCCESSFUL_JOB_STATE_ERROR",
)


Expand Down
Loading
Loading