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 28, 2024
1 parent df53de3 commit 511b9b3
Show file tree
Hide file tree
Showing 60 changed files with 1,741 additions and 712 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
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 @@ -164,9 +164,7 @@ def get_eclipse_session_configuration() -> (
else "http://localhost:8080"
"{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
1 change: 1 addition & 0 deletions backend/capellacollab/core/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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.users.models
import capellacollab.sessions.models
Expand Down
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
)
)
44 changes: 44 additions & 0 deletions backend/capellacollab/projects/toolmodels/provisioning/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# 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.core.authentication import injectables as auth_injectables
from capellacollab.projects.users import models as projects_users_models

from . import crud, injectables, models

router = fastapi.APIRouter(
dependencies=[
fastapi.Depends(
auth_injectables.ProjectRoleVerification(
required_role=projects_users_models.ProjectUserRole.USER
)
)
],
)


@router.get("", response_model=models.ModelProvisioning | None)
def get_provisioning(
provisioning: models.DatabaseModelProvisioning = fastapi.Depends(
injectables.get_model_provisioning
),
) -> models.DatabaseModelProvisioning:
return provisioning


@router.delete("", status_code=204)
def reset_provisioning(
provisioning: models.DatabaseModelProvisioning = fastapi.Depends(
injectables.get_model_provisioning
),
db: orm.Session = fastapi.Depends(database.get_db),
):
"""This will delete the provisioning data from the workspace.
During the next session request, the existing provisioning will be overwritten in the workspace.
"""

crud.delete_project_provisioning(db, provisioning)
6 changes: 6 additions & 0 deletions backend/capellacollab/projects/toolmodels/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .diagrams import routes as diagrams_routes
from .modelbadge import routes as complexity_badge_routes
from .modelsources import routes as modelsources_routes
from .provisioning import routes as provisioning_routes
from .restrictions import routes as restrictions_routes

router = fastapi.APIRouter(
Expand Down Expand Up @@ -267,3 +268,8 @@ def raise_if_model_exists_in_project(
prefix="/{model_slug}/badges/complexity",
tags=["Projects - Models - Model complexity badge"],
)
router.include_router(
provisioning_routes.router,
prefix="/{model_slug}/provisioning",
tags=["Projects - Models - Provisioning"],
)
8 changes: 3 additions & 5 deletions backend/capellacollab/sessions/hooks/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,16 @@


class PreAuthenticationHook(interface.HookRegistration):
def session_connection_hook( # type: ignore[override]
def session_connection_hook(
self,
db_session: sessions_models.DatabaseSession,
user: users_models.DatabaseUser,
**kwargs,
request: interface.SessionConnectionHookRequest,
) -> interface.SessionConnectionHookResult:
"""Issue pre-authentication tokens for sessions"""

return interface.SessionConnectionHookResult(
cookies={
"ccm_session_token": self._issue_session_token(
user, db_session
request.user, request.db_session
)
}
)
Expand Down
Loading

0 comments on commit 511b9b3

Please sign in to comment.