majuna/app/portal/static.py

267 lines
10 KiB
Python
Raw Normal View History

import logging
from typing import Any, List, Optional
import sqlalchemy.exc
2024-12-06 18:15:47 +00:00
from flask import (
Blueprint,
Response,
current_app,
flash,
redirect,
render_template,
url_for,
)
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
from sqlalchemy import exc
2024-12-06 18:15:47 +00:00
from wtforms import BooleanField, FileField, SelectField, StringField, SubmitField
from wtforms.validators import DataRequired
from app.brm.static import create_static_origin
from app.models.base import Group
from app.models.cloud import CloudAccount, CloudProvider
from app.models.mirrors import Origin, StaticOrigin
from app.portal.util import response_404, view_lifecycle
bp = Blueprint("static", __name__)
class StaticOriginForm(FlaskForm): # type: ignore
description = StringField(
2024-12-06 18:15:47 +00:00
"Description",
validators=[DataRequired()],
2024-12-06 18:15:47 +00:00
description="Enter a brief description of the static website that you are creating in this field. This is "
"also a required field.",
)
group = SelectField(
2024-12-06 18:15:47 +00:00
"Group",
validators=[DataRequired()],
2024-12-06 18:15:47 +00:00
description="Select the group that you want the origin to belong to from the drop-down menu in this field. "
"This is a required field.",
)
storage_cloud_account = SelectField(
2024-12-06 18:15:47 +00:00
"Storage Cloud Account",
validators=[DataRequired()],
2024-12-06 18:15:47 +00:00
description="Select the cloud account that you want the origin to be deployed to from the drop-down menu in "
"this field. This is a required field.",
)
source_cloud_account = SelectField(
2024-12-06 18:15:47 +00:00
"Source Cloud Account",
validators=[DataRequired()],
2024-12-06 18:15:47 +00:00
description="Select the cloud account that will be used to modify the source repository for the web content "
"for this static origin. This is a required field.",
)
source_project = StringField(
2024-12-06 18:15:47 +00:00
"Source Project",
validators=[DataRequired()],
2024-12-06 18:15:47 +00:00
description="GitLab project path.",
)
auto_rotate = BooleanField(
2024-12-06 18:15:47 +00:00
"Auto-Rotate",
default=True,
2024-12-06 18:15:47 +00:00
description="Select this field if you want to enable auto-rotation for the mirror. This means that the mirror "
"will automatically redeploy with a new domain name if it is detected to be blocked. This field "
"is optional and is enabled by default.",
)
matrix_homeserver = SelectField(
2024-12-06 18:15:47 +00:00
"Matrix Homeserver",
description="Select the Matrix homeserver from the drop-down box to enable Keanu Convene on mirrors of this "
"static origin.",
)
keanu_convene_path = StringField(
2024-12-06 18:15:47 +00:00
"Keanu Convene Path",
default="talk",
description="Enter the subdirectory to present the Keanu Convene application at on the mirror. This defaults "
'to "talk".',
)
keanu_convene_logo = FileField(
2024-12-06 18:15:47 +00:00
"Keanu Convene Logo", description="Logo to use for Keanu Convene"
)
keanu_convene_color = StringField(
2024-12-06 18:15:47 +00:00
"Keanu Convene Accent Color",
default="#0047ab",
description="Accent color to use for Keanu Convene (HTML hex code)",
)
enable_clean_insights = BooleanField(
2024-12-06 18:15:47 +00:00
"Enable Clean Insights",
description="When enabled, a Clean Insights Measurement Proxy endpoint is deployed on the mirror to allow for "
"submission of results from any of the supported Clean Insights SDKs.",
)
2024-12-06 18:15:47 +00:00
submit = SubmitField("Save Changes")
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
2024-12-06 18:15:47 +00:00
self.storage_cloud_account.choices = [
(x.id, f"{x.provider.description} - {x.description}")
for x in CloudAccount.query.filter(
CloudAccount.provider == CloudProvider.AWS
).all()
]
self.source_cloud_account.choices = [
(x.id, f"{x.provider.description} - {x.description}")
for x in CloudAccount.query.filter(
CloudAccount.provider == CloudProvider.GITLAB
).all()
]
self.matrix_homeserver.choices = [
(x, x) for x in current_app.config["MATRIX_HOMESERVERS"]
]
2024-12-06 18:15:47 +00:00
@bp.route("/new", methods=["GET", "POST"])
@bp.route("/new/<group_id>", methods=["GET", "POST"])
def static_new(group_id: Optional[int] = None) -> ResponseReturnValue:
form = StaticOriginForm()
2024-12-06 18:15:47 +00:00
if (
len(form.source_cloud_account.choices) == 0
or len(form.storage_cloud_account.choices) == 0
):
flash(
"You must add at least one AWS account and at least one GitLab account before creating static origins.",
"warning",
)
return redirect(url_for("portal.cloud.cloud_account_list"))
if form.validate_on_submit():
try:
static = create_static_origin(
form.description.data,
int(form.group.data),
int(form.storage_cloud_account.data),
int(form.source_cloud_account.data),
form.source_project.data,
form.auto_rotate.data,
form.matrix_homeserver.data,
form.keanu_convene_path.data,
form.keanu_convene_logo.data,
form.keanu_convene_color.data,
form.enable_clean_insights.data,
2024-12-06 18:15:47 +00:00
True,
)
flash(f"Created new static origin #{static.id}.", "success")
return redirect(url_for("portal.static.static_edit", static_id=static.id))
2024-12-06 18:15:47 +00:00
except (
ValueError
) as e: # may be returned by create_static_origin and from the int conversion
logging.warning(e)
2024-12-06 18:15:47 +00:00
flash(
"Failed to create new static origin due to an invalid input.", "danger"
)
return redirect(url_for("portal.static.static_list"))
except exc.SQLAlchemyError as e:
2024-12-06 18:15:47 +00:00
flash(
"Failed to create new static origin due to a database error.", "danger"
)
logging.warning(e)
return redirect(url_for("portal.static.static_list"))
if group_id:
form.group.data = group_id
return render_template("new.html.j2", section="static", form=form)
2024-12-06 18:15:47 +00:00
@bp.route("/edit/<static_id>", methods=["GET", "POST"])
def static_edit(static_id: int) -> ResponseReturnValue:
2024-12-06 18:15:47 +00:00
static_origin: Optional[StaticOrigin] = StaticOrigin.query.filter(
StaticOrigin.id == static_id
).first()
if static_origin is None:
2024-12-06 18:15:47 +00:00
return Response(
render_template(
"error.html.j2",
section="static",
header="404 Origin Not Found",
message="The requested static origin could not be found.",
),
status=404,
)
form = StaticOriginForm(
description=static_origin.description,
group=static_origin.group_id,
storage_cloud_account=static_origin.storage_cloud_account_id,
source_cloud_account=static_origin.source_cloud_account_id,
source_project=static_origin.source_project,
matrix_homeserver=static_origin.matrix_homeserver,
keanu_convene_path=static_origin.keanu_convene_path,
auto_rotate=static_origin.auto_rotate,
enable_clean_insights=bool(static_origin.clean_insights_backend),
)
form.group.render_kw = {"disabled": ""}
form.storage_cloud_account.render_kw = {"disabled": ""}
form.source_cloud_account.render_kw = {"disabled": ""}
if form.validate_on_submit():
try:
static_origin.update(
form.source_project.data,
form.description.data,
form.auto_rotate.data,
form.matrix_homeserver.data,
form.keanu_convene_path.data,
form.keanu_convene_logo.data,
form.keanu_convene_color.data,
form.enable_clean_insights.data,
2024-12-06 18:15:47 +00:00
True,
)
flash("Saved changes to group.", "success")
2024-12-06 18:15:47 +00:00
except (
ValueError
) as e: # may be returned by create_static_origin and from the int conversion
logging.warning(e)
2024-12-06 18:15:47 +00:00
flash(
"An error occurred saving the changes to the static origin due to an invalid input.",
"danger",
)
except exc.SQLAlchemyError as e:
logging.warning(e)
2024-12-06 18:15:47 +00:00
flash(
"An error occurred saving the changes to the static origin due to a database error.",
"danger",
)
try:
2024-12-06 18:15:47 +00:00
origin = Origin.query.filter_by(
domain_name=static_origin.origin_domain_name
).one()
proxies = origin.proxies
except sqlalchemy.exc.NoResultFound:
proxies = []
2024-12-06 18:15:47 +00:00
return render_template(
"static.html.j2",
section="static",
static=static_origin,
form=form,
proxies=proxies,
)
@bp.route("/list")
def static_list() -> ResponseReturnValue:
2024-12-06 18:15:47 +00:00
statics: List[StaticOrigin] = StaticOrigin.query.order_by(
StaticOrigin.description
).all()
return render_template(
"list.html.j2",
section="static",
title="Static Origins",
item="static",
new_link=url_for("portal.static.static_new"),
items=statics,
)
2024-12-06 18:15:47 +00:00
@bp.route("/destroy/<static_id>", methods=["GET", "POST"])
def static_destroy(static_id: int) -> ResponseReturnValue:
2024-12-06 18:15:47 +00:00
static = StaticOrigin.query.filter(
StaticOrigin.id == static_id, StaticOrigin.destroyed.is_(None)
).first()
if static is None:
return response_404("The requested static origin could not be found.")
return view_lifecycle(
header=f"Destroy static origin {static.description}",
message=static.description,
success_message="All proxies from the destroyed static origin will shortly be destroyed at their providers, "
2024-12-06 18:15:47 +00:00
"and the static content will be removed from the cloud provider.",
success_view="portal.static.static_list",
section="static",
resource=static,
2024-12-06 18:15:47 +00:00
action="destroy",
)