mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-28 15:47:50 +09:00
Comments, docstrings and a few strings cited plan numbers ("plan 29
#9", "plan 86 §B2") from a git-ignored folder, so they pointed at
nothing; one reached users in a bottleneck hint. Code and frontend
strings are unchanged apart from those citations (checked by AST and
token comparison). A vhost still carrying the old asset comment reads
as undrifted, and a guard test keeps new citations out.
88 lines
4.1 KiB
Python
88 lines
4.1 KiB
Python
from datetime import datetime
|
|
|
|
from app import db
|
|
from app.models.mixins import EncryptedSecret, SoftDeleteMixin
|
|
|
|
# Engines whose lifecycle ServerKit actually drives (Mongo stays read-first).
|
|
VALID_DB_ENGINES = ('mysql', 'postgresql', 'mongodb')
|
|
|
|
|
|
class ManagedDatabase(SoftDeleteMixin, db.Model):
|
|
"""A database ServerKit tracks as a first-class resource.
|
|
|
|
Adds durable state (backups, connection strings, ownership) **beside** the
|
|
live introspection in ``database_service`` — it does NOT replace that
|
|
discovery/browse surface. ``origin`` records whether ServerKit provisioned it
|
|
(``provisioned``) or adopted a live-discovered one (``adopted``).
|
|
|
|
The admin secret is Fernet-encrypted at rest (same helper as
|
|
``SourceConnection``); it is masked in API responses and revealed only through
|
|
an explicit, audited action.
|
|
|
|
Soft-deletable: a plain delete tombstones the row into the
|
|
Recycle Bin — the actual database server-side is untouched and any managed
|
|
backup policy keeps running, so restore brings everything back intact. The
|
|
explicit ``?drop=true`` path stays immediately destructive. Read paths use
|
|
``query_active()``.
|
|
"""
|
|
|
|
__tablename__ = 'managed_databases'
|
|
|
|
# Default listening ports per engine, used when a row doesn't pin one.
|
|
DEFAULT_PORTS = {'mysql': 3306, 'postgresql': 5432, 'mongodb': 27017}
|
|
# URI scheme per engine for build_connection_uri.
|
|
URI_SCHEMES = {'mysql': 'mysql', 'postgresql': 'postgresql', 'mongodb': 'mongodb'}
|
|
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
engine = db.Column(db.String(20), nullable=False, index=True) # mysql|postgresql|mongodb
|
|
name = db.Column(db.String(200), nullable=False) # database name on the server
|
|
host_kind = db.Column(db.String(20), nullable=False, default='host') # host|docker
|
|
container_ref = db.Column(db.String(200), nullable=True) # container name/id when docker
|
|
host = db.Column(db.String(255), nullable=False, default='localhost')
|
|
port = db.Column(db.Integer, nullable=True)
|
|
# The app this DB backs, if any. SET NULL on app delete so the tracked DB
|
|
# survives its app being removed.
|
|
owner_application_id = db.Column(
|
|
db.Integer, db.ForeignKey('applications.id', ondelete='SET NULL'),
|
|
nullable=True, index=True,
|
|
)
|
|
origin = db.Column(db.String(20), nullable=False, default='provisioned') # provisioned|adopted
|
|
admin_username = db.Column(db.String(180), nullable=True)
|
|
admin_secret_encrypted = db.Column(db.Text, nullable=True)
|
|
admin_secret = EncryptedSecret('admin_secret_encrypted', legacy_plaintext=True)
|
|
workspace_id = db.Column(db.Integer, db.ForeignKey('workspaces.id'), nullable=True, index=True)
|
|
created_at = db.Column(db.DateTime, default=datetime.utcnow, index=True)
|
|
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint('engine', 'host', 'name', name='uq_managed_database'),
|
|
)
|
|
|
|
def effective_port(self):
|
|
return self.port or self.DEFAULT_PORTS.get(self.engine)
|
|
|
|
def to_dict(self, connection_uri=None):
|
|
data = {
|
|
'id': self.id,
|
|
'engine': self.engine,
|
|
'name': self.name,
|
|
'host_kind': self.host_kind,
|
|
'container_ref': self.container_ref,
|
|
'host': self.host,
|
|
'port': self.effective_port(),
|
|
'owner_application_id': self.owner_application_id,
|
|
'origin': self.origin,
|
|
'admin_username': self.admin_username,
|
|
# Never serialize the secret — expose only whether one is stored.
|
|
'has_secret': bool(self.admin_secret_encrypted),
|
|
'workspace_id': self.workspace_id,
|
|
'created_at': self.created_at.isoformat() if self.created_at else None,
|
|
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
|
|
}
|
|
if connection_uri is not None:
|
|
data['connection_uri'] = connection_uri
|
|
return data
|
|
|
|
def __repr__(self):
|
|
return f'<ManagedDatabase {self.engine}:{self.name}@{self.host}>'
|