import logging from typing import Any, List, Optional import sqlalchemy.exc 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 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( "Description", validators=[DataRequired()], description="Enter a brief description of the static website that you are creating in this field. This is " "also a required field.", ) group = SelectField( "Group", validators=[DataRequired()], 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( "Storage Cloud Account", validators=[DataRequired()], 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( "Source Cloud Account", validators=[DataRequired()], 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( "Source Project", validators=[DataRequired()], description="GitLab project path.", ) auto_rotate = BooleanField( "Auto-Rotate", default=True, 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( "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( "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( "Keanu Convene Logo", description="Logo to use for Keanu Convene" ) keanu_convene_color = StringField( "Keanu Convene Accent Color", default="#0047ab", description="Accent color to use for Keanu Convene (HTML hex code)", ) enable_clean_insights = BooleanField( "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.", ) 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()] 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"] ] @bp.route("/new", methods=["GET", "POST"]) @bp.route("/new/", methods=["GET", "POST"]) def static_new(group_id: Optional[int] = None) -> ResponseReturnValue: form = StaticOriginForm() 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, True, ) flash(f"Created new static origin #{static.id}.", "success") return redirect(url_for("portal.static.static_edit", static_id=static.id)) except ( ValueError ) as e: # may be returned by create_static_origin and from the int conversion logging.warning(e) 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: 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) @bp.route("/edit/", methods=["GET", "POST"]) def static_edit(static_id: int) -> ResponseReturnValue: static_origin: Optional[StaticOrigin] = StaticOrigin.query.filter( StaticOrigin.id == static_id ).first() if static_origin is None: 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, True, ) flash("Saved changes to group.", "success") except ( ValueError ) as e: # may be returned by create_static_origin and from the int conversion logging.warning(e) 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) flash( "An error occurred saving the changes to the static origin due to a database error.", "danger", ) try: origin = Origin.query.filter_by( domain_name=static_origin.origin_domain_name ).one() proxies = origin.proxies except sqlalchemy.exc.NoResultFound: proxies = [] return render_template( "static.html.j2", section="static", static=static_origin, form=form, proxies=proxies, ) @bp.route("/list") def static_list() -> ResponseReturnValue: 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, ) @bp.route("/destroy/", methods=["GET", "POST"]) def static_destroy(static_id: int) -> ResponseReturnValue: 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, " "and the static content will be removed from the cloud provider.", success_view="portal.static.static_list", section="static", resource=static, action="destroy", )