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 13, 2024
1 parent 1b940f4 commit 57ff990
Show file tree
Hide file tree
Showing 109 changed files with 11,319 additions and 6,943 deletions.
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: 320c5b39c509
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 = "320c5b39c509"
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"),
)
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...")

Check warning on line 273 in backend/capellacollab/projects/toolmodels/backups/runs/interface.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/backups/runs/interface.py#L273

Added line #L273 was not covered by tests
_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.")

Check warning on line 305 in backend/capellacollab/projects/toolmodels/backups/runs/interface.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/backups/runs/interface.py#L305

Added line #L305 was not covered by tests
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
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(

Check warning on line 72 in backend/capellacollab/projects/toolmodels/modelsources/git/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/modelsources/git/routes.py#L72

Added line #L72 was not covered by tests
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(

Check warning on line 97 in backend/capellacollab/projects/toolmodels/modelsources/git/routes.py

View check run for this annotation

Codecov / codecov/patch

backend/capellacollab/projects/toolmodels/modelsources/git/routes.py#L97

Added line #L97 was not covered by tests
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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

from fastapi import status

from capellacollab.core import exceptions as core_exceptions


class ProvisioningNotFoundError(core_exceptions.BaseError):
def __init__(self, project_slug: str, model_slug: str):
super().__init__(
status_code=status.HTTP_404_NOT_FOUND,
title="Provisioning not found",
reason=f"Couldn't find a provisioning for the model '{model_slug}' in the project '{project_slug}'.",
err_code="PROVISIONING_NOT_FOUND",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0


import fastapi
from sqlalchemy import orm

from capellacollab.core import database
from capellacollab.users import injectables as users_injectables
from capellacollab.users import models as users_models

from .. import injectables as toolmodels_injectables
from .. import models as toolmodels_models
from . import crud, models


def get_model_provisioning(
model: toolmodels_models.DatabaseToolModel = fastapi.Depends(
toolmodels_injectables.get_existing_capella_model
),
current_user: users_models.DatabaseUser = fastapi.Depends(
users_injectables.get_own_user
),
db: orm.Session = fastapi.Depends(database.get_db),
) -> models.DatabaseModelProvisioning | None:
return crud.get_model_provisioning(db, tool_model=model, user=current_user)
Loading

0 comments on commit 57ff990

Please sign in to comment.