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 Oct 29, 2024
1 parent 3727209 commit f1d1df7
Show file tree
Hide file tree
Showing 89 changed files with 4,431 additions and 742 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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
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 @@ -166,9 +166,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 @@ -133,5 +136,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 @@ -205,3 +206,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 @@ -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_project_provisioning(
db: orm.Session, model: models.DatabaseModelProvisioning
) -> models.DatabaseModelProvisioning:
db.add(model)
db.commit()
return model


def get_project_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_project_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,28 @@
# 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_project_provisioning(
db, tool_model=model, user=current_user
)
66 changes: 66 additions & 0 deletions backend/capellacollab/projects/toolmodels/provisioning/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import datetime

import pydantic
import sqlalchemy as sa
from sqlalchemy import orm

from capellacollab.core import database
from capellacollab.core import pydantic as core_pydantic
from capellacollab.projects.toolmodels import (
models as projects_toolmodels_models,
)
from capellacollab.sessions import models as sessions_models
from capellacollab.users import models as users_models


class ModelProvisioning(core_pydantic.BaseModel):
session: sessions_models.Session | None
provisioned_at: datetime.datetime
revision: str
commit_hash: str

_validate_trigger_time = pydantic.field_serializer("provisioned_at")(
core_pydantic.datetime_serializer
)


class DatabaseModelProvisioning(database.Base):
__tablename__ = "model_provisioning"

id: orm.Mapped[int] = orm.mapped_column(
init=False, primary_key=True, index=True
)

user_id: orm.Mapped[int] = orm.mapped_column(
sa.ForeignKey("users.id"),
init=False,
)
user: orm.Mapped[users_models.DatabaseUser] = orm.relationship(
foreign_keys=[user_id]
)

tool_model_id: orm.Mapped[int] = orm.mapped_column(
sa.ForeignKey("models.id"),
init=False,
)
tool_model: orm.Mapped[projects_toolmodels_models.DatabaseToolModel] = (
orm.relationship(
foreign_keys=[tool_model_id],
)
)

revision: orm.Mapped[str]
commit_hash: orm.Mapped[str]

provisioned_at: orm.Mapped[datetime.datetime] = orm.mapped_column(
default=datetime.datetime.now(datetime.UTC)
)

session: orm.Mapped[sessions_models.DatabaseSession | None] = (
orm.relationship(
uselist=False, back_populates="provisioning", default=None
)
)
Loading

0 comments on commit f1d1df7

Please sign in to comment.