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 18, 2024
1 parent ce0f9de commit 1a5b4b4
Show file tree
Hide file tree
Showing 40 changed files with 933 additions and 622 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: 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"]
)
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
36 changes: 36 additions & 0 deletions backend/capellacollab/projects/toolmodels/provisioning/crud.py
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

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
):
db.add(model)
db.commit()


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
)
60 changes: 60 additions & 0 deletions backend/capellacollab/projects/toolmodels/provisioning/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors
# SPDX-License-Identifier: Apache-2.0

import datetime

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: datetime.datetime


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)
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"],
)
39 changes: 14 additions & 25 deletions backend/capellacollab/sessions/hooks/guacamole.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@

from capellacollab.config import config
from capellacollab.core import credentials
from capellacollab.sessions import models as sessions_models
from capellacollab.sessions.operators import k8s
from capellacollab.tools import models as tools_models

from . import interface

Expand All @@ -40,14 +37,11 @@ class GuacamoleIntegration(interface.HookRegistration):
"https": None,
}

def post_session_creation_hook( # type: ignore[override]
def post_session_creation_hook(
self,
session: k8s.Session,
db_session: sessions_models.DatabaseSession,
connection_method: tools_models.ToolSessionConnectionMethod,
**kwargs,
request: interface.PostSessionCreationHookRequest,
) -> interface.PostSessionCreationHookResult:
if connection_method.type != "guacamole":
if request.connection_method.type != "guacamole":
return interface.PostSessionCreationHookResult()

guacamole_username = credentials.generate_password()
Expand All @@ -60,9 +54,9 @@ def post_session_creation_hook( # type: ignore[override]

guacamole_identifier = self._create_connection(
guacamole_token,
db_session.environment["CAPELLACOLLAB_SESSION_TOKEN"],
session["host"],
session["port"],
request.db_session.environment["CAPELLACOLLAB_SESSION_TOKEN"],
request.session["host"],
request.session["port"],
)["identifier"]

self._assign_user_to_connection(
Expand All @@ -79,16 +73,14 @@ def post_session_creation_hook( # type: ignore[override]
config=guacamole_config,
)

def session_connection_hook( # type: ignore[override]
def session_connection_hook(
self,
db_session: sessions_models.DatabaseSession,
connection_method: tools_models.ToolSessionConnectionMethod,
**kwargs,
request: interface.SessionConnectionHookRequest,
) -> interface.SessionConnectionHookResult:
if connection_method.type != "guacamole":
if request.connection_method.type != "guacamole":
return interface.SessionConnectionHookResult()

session_config = db_session.config
session_config = request.db_session.config

if not session_config or not session_config.get("guacamole_username"):
return interface.SessionConnectionHookResult()
Expand All @@ -102,16 +94,13 @@ def session_connection_hook( # type: ignore[override]
redirect_url=config.extensions.guacamole.public_uri + "/#/",
)

def pre_session_termination_hook( # type: ignore[override]
self,
session: sessions_models.DatabaseSession,
connection_method: tools_models.ToolSessionConnectionMethod,
**kwargs,
def pre_session_termination_hook(
self, request: interface.PreSessionTerminationHookRequest
) -> interface.PreSessionTerminationHookResult:
if connection_method.type != "guacamole":
if request.connection_method.type != "guacamole":
return interface.SessionConnectionHookResult()

session_config = session.config
session_config = request.session.config

if session_config and session_config.get("guacamole_username"):
guacamole_token = self._get_admin_token()
Expand Down
Loading

0 comments on commit 1a5b4b4

Please sign in to comment.