import logging from typing import Optional from rest_framework.response import Response from rest_framework.permissions import ( IsAuthenticated, IsAdminUser, AllowAny, ) from rest_framework.request import Request from rest_framework import status from django.utils.translation import gettext as _ from django.http import HttpResponseRedirect from django.contrib.auth import REDIRECT_FIELD_NAME from labs.lab_manager_v2.lab_instance import ( LabInstance, redeem_lab, get_active_lab_instance, check_user_can_start_lab, ) from labs.lab_manager_v2.lab_spec import LabSpec from labs.models import ( LabQuiz as DBLabQuiz, LabQuizSubmission as DBLabQuizSubmission, LabTask as DBLabTask, LabTaskDependency as DBLabTaskDependency, ) from labs.lab_manager_v2.exceptions import ( LabRedeemFailedError, LabGuideNotFoundError, LabAccessForbiddenError, LabExitForbiddenError, ) from labs.api.base import Endpoint from labs.api.bases.lab_v2 import ( LabDetailBaseEndpoint, LabInstanceDetailBaseEndpoint, LabInstanceQuizBaseEndpoint, ) from labs.api.serializers.lab import ( CreateLabSerializer, LabGuidePartQuerySerializer, BulkGenerateCreditsVoucherSerializer, ) from labs.api.serializers.lab_spec import LabSpecSerializer from labs.api.serializers.lab_guide import LabGuideUploadImageSerializer from labs.api.serializers.lab_quiz import ( LabQuizSubmissionGetSerializer, LabQuizSubmissionPostSerializer, ) from labs.billing.voucher import bulk_generate_credits_voucher logger = logging.getLogger(__name__) class LabDetailEndpoint(LabDetailBaseEndpoint): permission_classes = (AllowAny,) def get( self, request: Request, lab_code: str, lab_spec: LabSpec, **kwargs, ) -> Response: meta = lab_spec.to_frontend( user=request.user, ) if request.user and request.user.is_authenticated: can_start_response = check_user_can_start_lab(request.user, lab_spec) meta.update( { "can_start": can_start_response.can_start, "suggestion": can_start_response.suggestion, } ) return Response(meta) class LabGuidePartEndpoint(LabDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get( self, request: Request, lab_code: str, lab_spec: LabSpec, **kwargs, ) -> Response: serializer = LabGuidePartQuerySerializer(data=request.query_params) serializer.is_valid(raise_exception=True) language_code = serializer.validated_data.get("language_code") try: data = lab_spec.get_lab_guide(language_code).get_parts(is_preview=True) except LabGuideNotFoundError: return Response( {"message": f"Lab guide not found for this language: {language_code}"}, status=status.HTTP_404_NOT_FOUND, ) return Response(data) class LabRedeemEndpoint(LabDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def post( self, request: Request, lab_code: str, lab_spec: LabSpec, **kwargs ) -> Response: try: redeem_lab(request.user, lab_code, lab_spec) except LabRedeemFailedError as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_400_BAD_REQUEST, ) return Response(status=status.HTTP_204_NO_CONTENT) class LabInstanceCreateEndpoint(Endpoint): permission_classes = (IsAuthenticated,) def post( self, request: Request, **kwargs, ) -> Response: serializer = CreateLabSerializer( data=request.data, context={"request": request} ) serializer.is_valid(raise_exception=True) tidb_version = serializer.validated_data.get("tidb_version") tidb_environment = serializer.validated_data.get("tidb_environment") region_name = serializer.validated_data.get("region_name") cloud_provider = serializer.validated_data.get("cloud_provider") lab_spec = serializer.lab_spec user_serverless_cluster = serializer.user_serverless_cluster try: lab_instance = LabInstance.start_new_lab( user=request.user, lab_spec=lab_spec, cloud_provider=cloud_provider, region_name=region_name, tidb_version=tidb_version, tidb_environment=tidb_environment, user_serverless_cluster=user_serverless_cluster, ) except LabAccessForbiddenError as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_400_BAD_REQUEST, ) return Response({"lab_instance_id": lab_instance.db_lab_instance.id}) class LabInstanceActiveEndpoint(Endpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, **kwargs) -> Response: lab_instance = get_active_lab_instance(request.user) if not lab_instance: return Response(status=status.HTTP_404_NOT_FOUND) return Response(lab_instance.get_detail()) class LabInstanceDetailEndpoint(LabInstanceDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, lab_instance: LabInstance) -> Response: return Response(lab_instance.get_detail()) def delete(self, request: Request, lab_instance: LabInstance) -> Response: try: lab_instance.exit_lab() except LabExitForbiddenError as e: return Response( {"non_field_errors": [str(e)]}, status=status.HTTP_409_CONFLICT, ) return Response(status=status.HTTP_204_NO_CONTENT) class LabInstanceGuidePartEndpoint(LabInstanceDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get( self, request: Request, lab_instance: LabInstance, **kwargs, ) -> Response: serializer = LabGuidePartQuerySerializer(data=request.query_params) serializer.is_valid(raise_exception=True) language_code = serializer.validated_data.get("language_code") try: return Response( lab_instance.lab_spec.get_lab_guide(language_code).get_parts( is_preview=False ) ) except LabGuideNotFoundError: return Response( {"message": f"Lab guide not found for this language: {language_code}"}, status=status.HTTP_404_NOT_FOUND, ) class LabInstanceResourceEndpoint(LabInstanceDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, lab_instance: LabInstance) -> Response: return Response(lab_instance.get_resources()) class LabInstanceEnvironmentVariablesEndpoint(LabInstanceDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, lab_instance: LabInstance) -> Response: return Response(lab_instance.get_environment_variables()) class LabInstanceHTTPSReverseProxyEndpoint(LabInstanceDetailBaseEndpoint): permission_classes = (IsAuthenticated,) def get(self, request: Request, lab_instance: LabInstance) -> Response: redirect_to = request.query_params.get(REDIRECT_FIELD_NAME) role = request.query_params.get("role") if role: instance = next( (ei for ei in lab_instance.lab_resource.ec2_instances if ei.role == role), None, ) if not instance: return Response( {"message": f"Invalid role: {role}"}, status=status.HTTP_400_BAD_REQUEST, ) if not instance.https_reverse_proxy: return Response( {"message": f"HTTPS reverse proxy is not enabled for role: {role}"}, status=status.HTTP_400_BAD_REQUEST, ) else: reverse_proxy_instances = [ ei for ei in lab_instance.lab_resource.ec2_instances if ei.https_reverse_proxy ] if not reverse_proxy_instances: return Response( {"message": "HTTPS reverse proxy is not enabled for this lab"}, status=status.HTTP_400_BAD_REQUEST, ) role = sorted(reverse_proxy_instances, key=lambda ei: ei.role)[0].role url = lab_instance.get_https_reverse_proxy_url(redirect_to, role) if not url: return Response( {"message": "Failed to get https reverse proxy url"}, status=status.HTTP_400_BAD_REQUEST, ) return HttpResponseRedirect(url) class LabInstanceQuizSubmissionEndpoint(LabInstanceQuizBaseEndpoint): permission_classes = (IsAuthenticated,) def get( self, request: Request, lab_instance: LabInstance, lab_quiz: DBLabQuiz, lab_quiz_submission: Optional[DBLabQuizSubmission], **kwargs, ) -> Response: if not lab_quiz_submission: return Response(status=status.HTTP_404_NOT_FOUND) return Response( LabQuizSubmissionGetSerializer( lab_quiz_submission, context={"lab_quiz": lab_quiz}, ).data, ) def post( self, request: Request, lab_instance: LabInstance, lab_quiz: DBLabQuiz, lab_quiz_submission: Optional[DBLabQuizSubmission], **kwargs, ) -> Response: serializer = LabQuizSubmissionPostSerializer( lab_quiz_submission, data=request.data, context={ "request": request, "lab_quiz": lab_quiz, "lab_instance": lab_instance.db_lab_instance, }, ) serializer.is_valid(raise_exception=True) lab_quiz_submission = serializer.save() return Response( LabQuizSubmissionGetSerializer( lab_quiz_submission, context={"lab_quiz": lab_quiz}, ).data, ) class LabInstanceCreateJobsListEndpoint(LabInstanceDetailBaseEndpoint): permission_classes = (IsAdminUser,) def get(self, request: Request, lab_instance: LabInstance) -> Response: db_lab_instance = lab_instance.db_lab_instance tasks = ( DBLabTask.objects.filter(lab_job=db_lab_instance.create_job) .values( "id", "name", "status", "payload", "retries", "comment", "started_at", "finished_at", "created_at", "updated_at", ) .order_by("created_at") ) task_dependencies = DBLabTaskDependency.objects.filter( lab_job=db_lab_instance.create_job ).values( "lab_task_id", "depends_on_id", ) return Response( { "tasks": tasks, "task_dependencies": task_dependencies, } ) class LabValidateSpecEndpoint(Endpoint): permission_classes = (IsAdminUser,) def post(self, request: Request) -> Response: serializer = LabSpecSerializer(data=request.data) serializer.is_valid(raise_exception=True) return Response(status=status.HTTP_200_OK) class LabGuideUploadEndpoint(Endpoint): permission_classes = (IsAdminUser,) def post(self, request: Request) -> Response: serializer = LabGuideUploadImageSerializer( data=request.data, context={"request": request}, ) serializer.is_valid(raise_exception=True) image_url = serializer.save() return Response( { "public_url": image_url, } ) class BulkGenerateCreditsVoucherEndpoint(Endpoint): permission_classes = (IsAdminUser,) def post(self, request: Request) -> Response: """ POST /api/labs/credits_vouchers { "redeemable_days": 180, "campaign_code": "701RC00000Let9mXXX", "credits_per_voucher": 200, "voucher_count": 5, "voucher_category": "partner - migration competency | leads - nurture | customer - deep dive | user - engagement | Others", "customer_name": "N/A" } """ serializer = BulkGenerateCreditsVoucherSerializer(data=request.data) serializer.is_valid(raise_exception=True) lark_user_union_id = serializer.validated_data.get("lark_user_union_id") redeemable_days = serializer.validated_data.get("redeemable_days") campaign_code = serializer.validated_data.get("campaign_code") credits_per_voucher = serializer.validated_data.get("credits_per_voucher") voucher_count = serializer.validated_data.get("voucher_count") voucher_category = serializer.validated_data.get("voucher_category") customer_name = serializer.validated_data.get("customer_name") response = bulk_generate_credits_voucher( user=request.user, lark_user_union_id=lark_user_union_id, redeemable_days=redeemable_days, campaign_code=campaign_code, credits_per_voucher=credits_per_voucher, voucher_count=voucher_count, voucher_category=voucher_category, customer_name=customer_name, ) if response: return Response(response) else: return Response( {"message": _("Please contact TiDB Labs admin for help.")}, status=status.HTTP_403_FORBIDDEN, )