Skip to content

Commit

Permalink
feat: Add support for provisioning of tool models
Browse files Browse the repository at this point in the history
The already known provisioning feature for read-only sessions will
be extended to persistent workspace sessions.

Per session, a user can request a persistent workspace session and can
pass optionally one model to provision. It's not possible to provision
more than one model (aka one Git repository).

The provisioning takes place once. We'll not touch the provisioned
workspace until the user explicently resets the provisioning.
The provisioning revision and date are stored in the database.

When the users reset their workspace, we'll remove the provisioning
model from the database. During the next session start, the matching
workspace will be re-initialized and a copy will be saved to a `.bak` directory.

This feature is essential for the start-up of trainings.
  • Loading branch information
MoritzWeber0 committed Nov 12, 2024
1 parent 4fe9a8d commit 0b80ff3
Show file tree
Hide file tree
Showing 152 changed files with 7,004 additions and 1,534 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
docker run --rm -v /tmp:/tmp:ro tufin/oasdiff changelog \
--format markup \
/tmp/openapi.json /tmp/openapi2.json \
| sed 's/\/anyOf\[subschema #[0-9]\+\(: [a-zA-Z]\+\)\?\]//g'
| sed 's/#\([0-9]\+\)/\1/g'
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- name: Find existing comment on PR
Expand Down
9 changes: 8 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,14 @@ repos:
- id: pylint
name: pylint
entry: pylint
args: [--rcfile=./backend/pyproject.toml]
args: [
'-rn', # Only display messages
'-sn', # Don't display the score
'--rcfile=./backend/pyproject.toml',
]
language: system
types: [python]
require_serial: true
files: '^backend'
exclude: '^backend/capellacollab/alembic/'
- repo: local
Expand All @@ -101,6 +106,8 @@ repos:
- 'prettier-plugin-tailwindcss@^0.6.8'
- '@trivago/prettier-plugin-sort-imports@^4.3.0'
- 'tailwindcss@^3.4.12'
- 'prettier-plugin-classnames@^0.7.4'
- 'prettier-plugin-merge@^0.7.1'
- repo: https://github.com/Lucas-C/pre-commit-hooks
rev: v1.5.5
hooks:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

"""Add provisioning feature
Revision ID: 014438261702
Revises: 3818a5009130
Create Date: 2024-10-11 17:34:05.210906
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "014438261702"
down_revision = "3818a5009130"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"model_provisioning",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("user_id", sa.Integer(), nullable=False),
sa.Column("tool_model_id", sa.Integer(), nullable=False),
sa.Column("revision", sa.String(), nullable=False),
sa.Column("commit_hash", sa.String(), nullable=False),
sa.Column("provisioned_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(
["tool_model_id"],
["models.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_model_provisioning_id"),
"model_provisioning",
["id"],
unique=False,
)
op.add_column(
"sessions", sa.Column("provisioning_id", sa.Integer(), nullable=True)
)
op.create_foreign_key(
None, "sessions", "model_provisioning", ["provisioning_id"], ["id"]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

"""Add project tools table
Revision ID: 2f8449c217fa
Revises: 014438261702
Create Date: 2024-10-29 14:11:47.774679
"""
import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "2f8449c217fa"
down_revision = "014438261702"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"project_tool_association",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("tool_version_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["project_id"],
["projects.id"],
),
sa.ForeignKeyConstraint(
["tool_version_id"],
["versions.id"],
),
sa.PrimaryKeyConstraint("id", "project_id", "tool_version_id"),
)
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ def get_eclipse_configuration():
"{CAPELLACOLLAB_SESSIONS_SCHEME}://{CAPELLACOLLAB_SESSIONS_HOST}:{CAPELLACOLLAB_SESSIONS_PORT}"
"{CAPELLACOLLAB_SESSIONS_BASE_PATH}/?floating_menu=0&path={CAPELLACOLLAB_SESSIONS_BASE_PATH}/"
),
"cookies": {
"token": "{CAPELLACOLLAB_SESSION_TOKEN}",
},
"cookies": {},
},
]
},
Expand Down
4 changes: 1 addition & 3 deletions backend/capellacollab/core/database/migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,7 @@ def get_eclipse_session_configuration() -> (
)
+ "{CAPELLACOLLAB_SESSIONS_BASE_PATH}/?floating_menu=0&path={CAPELLACOLLAB_SESSIONS_BASE_PATH}/"
),
cookies={
"token": "{CAPELLACOLLAB_SESSION_TOKEN}",
},
cookies={},
sharing=tools_models.ToolSessionSharingConfiguration(
enabled=True
),
Expand Down
2 changes: 2 additions & 0 deletions backend/capellacollab/core/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import capellacollab.projects.toolmodels.models
import capellacollab.projects.toolmodels.modelsources.git.models
import capellacollab.projects.toolmodels.modelsources.t4c.models
import capellacollab.projects.toolmodels.provisioning.models
import capellacollab.projects.toolmodels.restrictions.models
import capellacollab.projects.tools.models
import capellacollab.projects.users.models
import capellacollab.sessions.models
import capellacollab.settings.configuration.models
Expand Down
30 changes: 30 additions & 0 deletions backend/capellacollab/core/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,33 @@ class ZIPFileResponse(fastapi.responses.StreamingResponse):
}
}
}


class MarkdownResponse(fastapi.responses.Response):
"""Custom error class for Markdown responses.
To use the class as response class, pass the following parameters
to the fastapi route definition.
```python
response_class=fastapi.responses.Response
responses=responses.MarkdownResponse.responses
```
Don't use Markdown as response_class as this will also change the
media type for all error responses, see:
https://github.com/tiangolo/fastapi/discussions/6799
To return an Markdown response in the route, use:
```python
return responses.MarkdownResponse(
content=b"# Hello World",
)
```
"""

media_type = "text/markdown"
responses: dict[int | str, dict[str, t.Any]] | None = {
200: {"content": {"text/markdown": {"schema": {"type": "string"}}}}
}
6 changes: 6 additions & 0 deletions backend/capellacollab/projects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

if t.TYPE_CHECKING:
from capellacollab.projects.toolmodels.models import DatabaseToolModel
from capellacollab.projects.tools.models import (
DatabaseProjectToolAssociation,
)
from capellacollab.projects.users.models import ProjectUserAssociation


Expand Down Expand Up @@ -134,5 +137,8 @@ class DatabaseProject(database.Base):
models: orm.Mapped[list[DatabaseToolModel]] = orm.relationship(
default_factory=list, back_populates="project"
)
tools: orm.Mapped[list[DatabaseProjectToolAssociation]] = orm.relationship(
default_factory=list, back_populates="project"
)

is_archived: orm.Mapped[bool] = orm.mapped_column(default=False)
6 changes: 6 additions & 0 deletions backend/capellacollab/projects/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from capellacollab.projects.toolmodels.backups import core as backups_core
from capellacollab.projects.toolmodels.backups import crud as backups_crud
from capellacollab.projects.toolmodels.backups import models as backups_models
from capellacollab.projects.tools import routes as projects_tools_routes
from capellacollab.projects.users import crud as projects_users_crud
from capellacollab.projects.users import models as projects_users_models
from capellacollab.projects.users import routes as projects_users_routes
Expand Down Expand Up @@ -206,3 +207,8 @@ def _delete_all_pipelines_for_project(
prefix="/{project_slug}/events",
tags=["Projects - Events"],
)
router.include_router(
projects_tools_routes.router,
prefix="/{project_slug}/tools",
tags=["Projects - Tools"],
)
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ def _job_is_finished(status: models.PipelineRunStatus):


def _refresh_and_trigger_pipeline_jobs():
log.debug("Starting to refresh and trigger pipeline jobs...")
_schedule_pending_jobs()
with database.SessionLocal() as db:
for run in crud.get_scheduled_or_running_pipelines(db):
Expand Down Expand Up @@ -301,3 +302,4 @@ def _refresh_and_trigger_pipeline_jobs():
_terminate_job(run)

db.commit()
log.debug("Finished refreshing and triggering of pipeline jobs.")
8 changes: 4 additions & 4 deletions backend/capellacollab/projects/toolmodels/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sqlalchemy as sa
from sqlalchemy import orm

from capellacollab.projects import models as projects_model
from capellacollab.projects import models as projects_models
from capellacollab.tools import models as tools_models

from . import models
Expand Down Expand Up @@ -68,7 +68,7 @@ def get_model_by_slugs(
.options(orm.joinedload(models.DatabaseToolModel.project))
.where(
models.DatabaseToolModel.project.has(
projects_model.DatabaseProject.slug == project_slug
projects_models.DatabaseProject.slug == project_slug
)
)
.where(models.DatabaseToolModel.slug == model_slug)
Expand All @@ -77,7 +77,7 @@ def get_model_by_slugs(

def create_model(
db: orm.Session,
project: projects_model.DatabaseProject,
project: projects_models.DatabaseProject,
post_model: models.PostToolModel,
tool: tools_models.DatabaseTool,
version: tools_models.DatabaseVersion | None = None,
Expand Down Expand Up @@ -135,7 +135,7 @@ def update_model(
name: str | None,
version: tools_models.DatabaseVersion | None,
nature: tools_models.DatabaseNature | None,
project: projects_model.DatabaseProject,
project: projects_models.DatabaseProject,
display_order: int | None,
) -> models.DatabaseToolModel:
model.version = version
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def __init__(self):
title="Model complexity badge not configured properly",
reason=(
"The model complexity badge is not configured properly. "
"Please contact your diagram cache administrator."
"Please contact your project admin or system administrator."
),
err_code="MODEL_COMPLEXITY_BADGE_NOT_CONFIGURED_PROPERLY",
)
16 changes: 16 additions & 0 deletions backend/capellacollab/projects/toolmodels/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
DatabaseVersion,
)

from .provisioning.models import DatabaseModelProvisioning
from .restrictions.models import DatabaseToolModelRestrictions


Expand Down Expand Up @@ -124,6 +125,14 @@ class DatabaseToolModel(database.Base):
)
)

provisioning: orm.Mapped[list[DatabaseModelProvisioning]] = (
orm.relationship(
back_populates="tool_model",
cascade="delete",
default_factory=list,
)
)


class ToolModel(core_pydantic.BaseModel):
id: int
Expand All @@ -144,3 +153,10 @@ class SimpleToolModel(core_pydantic.BaseModel):
slug: str
name: str
project: projects_models.SimpleProject


class SimpleToolModelWithoutProject(core_pydantic.BaseModel):
id: int
slug: str
name: str
git_models: list[GitModel] | None = None
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from capellacollab.projects.toolmodels import models as toolmodels_models
from capellacollab.projects.toolmodels.backups import crud as backups_crud
from capellacollab.projects.users import models as projects_users_models
from capellacollab.settings.modelsources.git import core as git_core
from capellacollab.settings.modelsources.git import core as instances_git_core
from capellacollab.settings.modelsources.git import models as git_models
from capellacollab.settings.modelsources.git import util as git_util

Expand Down Expand Up @@ -69,7 +69,7 @@ async def get_revisions_of_primary_git_model(
injectables.get_existing_primary_git_model
),
) -> git_models.GetRevisionsResponseModel:
return await git_core.get_remote_refs(
return await instances_git_core.get_remote_refs(
primary_git_model.path,
primary_git_model.username,
primary_git_model.password,
Expand All @@ -94,7 +94,7 @@ async def get_revisions_with_model_credentials(
injectables.get_existing_git_model
),
):
return await git_core.get_remote_refs(
return await instances_git_core.get_remote_refs(
url, git_model.username, git_model.password
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0
37 changes: 37 additions & 0 deletions backend/capellacollab/projects/toolmodels/provisioning/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import sqlalchemy as sa
from sqlalchemy import orm

from capellacollab.projects.toolmodels import models as toolmodels_models
from capellacollab.users import models as users_models

from . import models


def create_model_provisioning(
db: orm.Session, model: models.DatabaseModelProvisioning
) -> models.DatabaseModelProvisioning:
db.add(model)
db.commit()
return model


def get_model_provisioning(
db: orm.Session,
tool_model: toolmodels_models.DatabaseToolModel,
user: users_models.DatabaseUser,
) -> models.DatabaseModelProvisioning | None:
return db.execute(
sa.select(models.DatabaseModelProvisioning)
.where(models.DatabaseModelProvisioning.tool_model == tool_model)
.where(models.DatabaseModelProvisioning.user == user)
).scalar_one_or_none()


def delete_model_provisioning(
db: orm.Session, provisioning: models.DatabaseModelProvisioning
):
db.delete(provisioning)
db.commit()
Loading

0 comments on commit 0b80ff3

Please sign in to comment.