Skip to content

Commit

Permalink
feat: Add support for plugins
Browse files Browse the repository at this point in the history
The System engineering toolchain team for Digitale Schiene is offering
several model modifiers and model derivative jobs.

Example for model modifiers are:
- ID injection bot (inject IDs into model elements)

Example for model derivates:
- Model complexity badge generation
- Diagram cache generation
- Automatic documentation generation

Synchronisation between TeamForCapella and Git is also considered as job,
somewhere in between.

Currently, all "jobs" are run in the Gitlab CI. There is no UI for the setup of these jobs.
Each job repository provides a Gitlab CI templates, which can be easily integrated in project repositories.
However, this approach is not intuitive for external people, leading to operation support effort for operation teams, which have to take care of the creation and updates of jobs.

Another disadavantage of the current approach is the Gitlab dependency.
Not all environments have a Gitlab instance available. Getting it running on other platforms is even more effort.

This commit adds a plugin/job store to the Capella Colloration Manager.
A plugin/job is still developed in a Git repository. Plugins have to define a
`mbse-works-plugin.yml`, providing metadata, input, output, trigger and job information.

The JSON schema can be fetched via `GET /api/v1.0/plugins-schema` (backend).
A human-readable documentation is available via `/docs/plugin-schema` (backend).

With this information, registered plugins can be easily integrated into projects.
The integration should be intuitive for project leads.

The steps are:
- Create a new pipeline
- Select a supported plugin from the store
- Configure the plugin: Supported types for the beginning are:
    - git (Select a linked git model from the project, so that the job can access the Git repository information)
    - t4c (Select a linked T4C repository/project from the project). The job gets the connection information + a session token.
    - yml (Most flexible option, the yml configuration file is mounted into the job container, validation via yml schema)
    - environment (Key/value pairs, which are used as environment variable for the job)
- Confirm the creation

As part of this commit, there are only two options:
- Run the pipeline manually
- Run it as 3am during the night.

Co-authored-by: ewuerger <[email protected]>
Co-authored-by: Paula-Kli <[email protected]>
  • Loading branch information
3 people committed Oct 31, 2023
1 parent 61a7b06 commit ec89338
Show file tree
Hide file tree
Showing 92 changed files with 3,793 additions and 2,380 deletions.
6 changes: 6 additions & 0 deletions backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,9 @@ load: clear

save:
docker run -i -e PGPASSWORD=$(DB_PASSWORD) --network host --entrypoint="pg_dump" postgres:latest -h 'localhost' -p $(DB_PORT) -U '$(DB_USER)' $(DB_NAME) > $(DATABASE_SAVE_DIR)/$(shell date +"%FT%T").sql

plugin-schema:
$(VENV)/bin/pip show json-schema-for-humans > /dev/null || $(VENV)/bin/pip install json-schema-for-humans
$(VENV)/bin/python -c 'import pathlib; import json; from capellacollab.plugins import models; pathlib.Path("plugin_schema.yml").write_text(json.dumps(models.PluginContent.model_json_schema()))'
mkdir -p plugin_schema
$(VENV)/bin/generate-schema-doc plugin_schema.yml plugin_schema/plugin_schema.html
17 changes: 10 additions & 7 deletions backend/capellacollab/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import fastapi_pagination
import starlette_prometheus
import uvicorn
from fastapi import middleware, responses
from fastapi import middleware, responses, staticfiles
from fastapi.middleware import cors

import capellacollab.projects.toolmodels.backups.runs.interface as pipeline_runs_interface
import capellacollab.projects.toolmodels.pipelines.runs.interface as pipeline_runs_interface
import capellacollab.sessions.metrics

# This import statement is required and should not be removed! (Alembic will not work otherwise)
Expand All @@ -21,12 +21,10 @@
from capellacollab.core import logging as core_logging
from capellacollab.core.database import engine, migration
from capellacollab.core.logging import exceptions as logging_exceptions
from capellacollab.plugins import schema as pipeline_schema
from capellacollab.projects.toolmodels import (
exceptions as toolmodels_exceptions,
)
from capellacollab.projects.toolmodels.backups import (
exceptions as backups_exceptions,
)
from capellacollab.projects.toolmodels.modelsources.git import (
exceptions as git_exceptions,
)
Expand All @@ -36,6 +34,9 @@
from capellacollab.projects.toolmodels.modelsources.git.handler import (
exceptions as git_handler_exceptions,
)
from capellacollab.projects.toolmodels.pipelines import (
exceptions as backups_exceptions,
)
from capellacollab.routes import router
from capellacollab.sessions import exceptions as sessions_exceptions
from capellacollab.sessions import idletimeout, operators
Expand Down Expand Up @@ -66,7 +67,7 @@ async def startup():
operators.get_operator()

logging.getLogger("uvicorn.access").disabled = True
logging.getLogger("uvicorn.error").disabled = True
logging.getLogger("uvicorn.error").disabled = False
logging.getLogger("requests_oauthlib.oauth2_session").setLevel("INFO")
logging.getLogger("kubernetes.client.rest").setLevel("INFO")

Expand All @@ -83,6 +84,7 @@ async def shutdown():
idletimeout.terminate_idle_sessions_in_background,
capellacollab.sessions.metrics.register,
pipeline_runs_interface.schedule_refresh_and_trigger_pipeline_jobs,
pipeline_schema.generate_schema_documentation,
],
middleware=[
middleware.Middleware(
Expand All @@ -98,7 +100,7 @@ async def shutdown():
middleware.Middleware(core_logging.LogRequestsMiddleware),
middleware.Middleware(starlette_prometheus.PrometheusMiddleware),
],
on_shutdown=[shutdown],
on_shutdown=[shutdown, pipeline_schema.cleanup_documentation_directory],
)

fastapi_pagination.add_pagination(app)
Expand Down Expand Up @@ -135,6 +137,7 @@ async def healthcheck():

app.add_route("/metrics", starlette_prometheus.metrics)
app.include_router(router, prefix="/api/v1")
pipeline_schema.mount_schema_documentation(app)


def register_exceptions():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

"""Add plugins table
Revision ID: e3f1006f6b49
Revises: d0cbf2813066
Create Date: 2023-05-26 11:56:13.928534
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "e3f1006f6b49"
down_revision = "ac0e6e0f77ee"
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
"plugins",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("username", sa.String(), nullable=True),
sa.Column("password", sa.String(), nullable=True),
sa.Column("remote", sa.String(), nullable=False),
sa.Column(
"content", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_plugins_id"), "plugins", ["id"], unique=True)
5 changes: 4 additions & 1 deletion backend/capellacollab/core/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@


class Base(orm.DeclarativeBase):
type_annotation_map = {dict[str, str]: postgresql.JSONB}
type_annotation_map = {
dict[str, str]: postgresql.JSONB,
dict[str, t.Any]: postgresql.JSONB,
}


### SQL MODELS ARE IMPORTED HERE ###
Expand Down
5 changes: 3 additions & 2 deletions backend/capellacollab/core/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
# These import statements of the models are required and should not be removed! (SQLAlchemy will not load the models otherwise)

import capellacollab.notices.models
import capellacollab.plugins.models
import capellacollab.projects.models
import capellacollab.projects.toolmodels.backups.models
import capellacollab.projects.toolmodels.backups.runs.models
import capellacollab.projects.toolmodels.models
import capellacollab.projects.toolmodels.modelsources.git.models
import capellacollab.projects.toolmodels.modelsources.t4c.models
import capellacollab.projects.toolmodels.pipelines.models
import capellacollab.projects.toolmodels.pipelines.runs.models
import capellacollab.projects.toolmodels.restrictions.models
import capellacollab.projects.users.models
import capellacollab.sessions.models
Expand Down
6 changes: 3 additions & 3 deletions backend/capellacollab/health/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

import pydantic

from capellacollab.projects.toolmodels.backups.runs import (
models as pipeline_run_models,
)
from capellacollab.projects.toolmodels.modelsources.git import (
models as git_models,
)
from capellacollab.projects.toolmodels.pipelines.runs import (
models as pipeline_run_models,
)


class StatusResponse(pydantic.BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion backend/capellacollab/health/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
import capellacollab.core.authentication.injectables as auth_injectables
import capellacollab.core.logging as core_logging
import capellacollab.projects.crud as projects_crud
import capellacollab.projects.toolmodels.backups.validation as pipelines_validation
import capellacollab.projects.toolmodels.crud as toolmodels_crud
import capellacollab.projects.toolmodels.diagrams.validation as diagrams_validation
import capellacollab.projects.toolmodels.modelbadge.validation as modelbadge_validation
import capellacollab.projects.toolmodels.modelsources.git.validation as git_validation
import capellacollab.projects.toolmodels.pipelines.validation as pipelines_validation
import capellacollab.projects.toolmodels.validation as toolmodels_validation
import capellacollab.projects.validation as projects_validation
import capellacollab.users.models as users_models
Expand Down
File renamed without changes.
62 changes: 62 additions & 0 deletions backend/capellacollab/plugins/crud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0


from collections import abc

import sqlalchemy as sa
from sqlalchemy import orm

from . import models


def get_plugins(db: orm.Session) -> abc.Sequence[models.DatabasePlugin]:
return db.execute(sa.select(models.DatabasePlugin)).scalars().all()


def get_plugin_by_id(
db: orm.Session, plugin_id: int
) -> models.DatabasePlugin | None:
return db.execute(
sa.select(models.DatabasePlugin).where(
models.DatabasePlugin.id == plugin_id
)
).scalar_one_or_none()


def create_plugin(
db: orm.Session,
remote: str,
username: str | None,
password: str | None,
content: dict | None,
) -> models.DatabasePlugin:
plugin = models.DatabasePlugin(
remote=remote, username=username, password=password, content=content
)
db.add(plugin)
db.commit()
return plugin


def update_plugin(
db: orm.Session,
plugin: models.DatabasePlugin,
patch_plugin: models.PatchPlugin,
content: str,
) -> models.DatabasePlugin:
if patch_plugin.username:
plugin.username = patch_plugin.username
if patch_plugin.password:
plugin.password = patch_plugin.password
if patch_plugin.remote:
plugin.remote = patch_plugin.remote
if content:
plugin.content = content
db.commit()
return plugin


def delete_plugin(db: orm.Session, plugin: models.DatabasePlugin) -> None:
db.delete(plugin)
db.commit()
23 changes: 23 additions & 0 deletions backend/capellacollab/plugins/injectables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: Copyright DB Netz AG and the capella-collab-manager contributors
# SPDX-License-Identifier: Apache-2.0

from fastapi import Depends, HTTPException
from sqlalchemy.orm import Session

from capellacollab.core import database

from . import crud, models


def get_existing_plugin(
plugin_id: int, db: Session = Depends(database.get_db)
) -> models.DatabasePlugin:
if plugin := crud.get_plugin_by_id(db, plugin_id):
return plugin

raise HTTPException(
404,
{
"reason": f"The plugin with the id {plugin_id} was not found.",
},
)
Loading

0 comments on commit ec89338

Please sign in to comment.