freeleaps-service-hub/app/authentication/backend/infra/code_management/depot_handler.py
2024-10-20 05:33:10 +00:00

417 lines
15 KiB
Python

import pytz
import random
from infra.log.module_logger import ModuleLogger
from typing import Optional
from app.authentication.backend.common.config.app_settings import app_settings
from app.authentication.backend.models.constants import DepotStatus
from app.authentication.backend.infra.code_management.gitea.gitea import Gitea
from app.authentication.backend.models.gitea.models import CodeDepotDoc
from infra.exception.exceptions import InvalidOperationError
from infra.utils.date import get_sunday
from datetime import datetime
from datetime import datetime, timedelta, timezone
from dateutil.parser import parse
class CodeDepotHandler:
def __init__(self) -> None:
self.gitea_url = app_settings.GITEA_URL
self.gitea_token = app_settings.GITEA_TOKEN
self.gitea_org = app_settings.GITEA_DEPOT_ORGANIZATION
self.code_depot_domain_name = app_settings.CODE_DEPOT_DOMAIN_NAME
self.code_depot_ssh_port = app_settings.CODE_DEPOT_SSH_PORT
self.code_depot_http_port = app_settings.CODE_DEPOT_HTTP_PORT
self.gitea_admin = Gitea(
self.gitea_url, token_text=self.gitea_token, auth=None, verify=False
)
self.product_org = self.gitea_admin.get_org_by_name(self.gitea_org)
self.module_logger = ModuleLogger(sender_id="CodeDepotManager")
async def check_depot_name_availabe(self, code_depot_name: str) -> bool:
"""Return True if the depot name is available, otherwise return False
Parameters:
code_depot_name (str): the name of the code depot
Returns:
bool: True if the depot name is availabe, otherwise return False
"""
result = await CodeDepotDoc.find_one(CodeDepotDoc.depot_name == code_depot_name)
if result:
return False
else:
return True
def check_code_depot_exist(self, code_depot_name: str) -> bool:
"""Check and return True if the code deport with the name exist in Gitea, otherwise return False
Parameters:
code_depot_name (str): the name of the code depot
Returns:
bool: True if the code depot exist, otherwise return False
"""
all_depots = self.product_org.get_repositories()
for depot in all_depots:
if depot.name.lower() == code_depot_name.lower():
return True
return False
async def __generate_new_code_depot_name(
self, code_depot_name: str, gitea_code_depot_names: list[str]
) -> str:
"""Generate a new code depot name if the code depot name already exists
Parameters:
code_depot_name (str): the name of the code depot
Returns:
str: the new code depot name
"""
code_depot_name = code_depot_name.lower()
depot_doc = await CodeDepotDoc.find_one(
CodeDepotDoc.depot_name == code_depot_name
)
# if the depot name already exists in the database or Gitea, generate a new name
if depot_doc or (code_depot_name in gitea_code_depot_names):
new_code_depot_name = "{}-{}".format(
code_depot_name, random.randint(10000, 99999)
)
while new_code_depot_name in gitea_code_depot_names:
new_code_depot_name = "{}-{}".format(
code_depot_name, random.randint(10000, 99999)
)
return await self.__generate_new_code_depot_name(
new_code_depot_name, gitea_code_depot_names
)
else:
return code_depot_name
async def create_code_depot(self, product_id, code_depot_name) -> Optional[str]:
"""Create a new git code depot
Parameters:
product_id (str): the id of the product
code_depot_name (str): the name of the code depot
Returns:
str: return code depot id if it's created successfully, else return None
"""
gitea_code_depot_names = [
depot.name.lower() for depot in self.product_org.get_repositories()
]
code_depot_name_to_be_created = await self.__generate_new_code_depot_name(
code_depot_name, gitea_code_depot_names
)
new_depot_doc = CodeDepotDoc(
depot_name=code_depot_name_to_be_created,
product_id=product_id,
depot_status=DepotStatus.TO_BE_CREATED,
)
await new_depot_doc.create()
try:
self.product_org.create_repo(
repoName=code_depot_name_to_be_created,
description=product_id,
private=True,
autoInit=True,
default_branch="main",
)
except Exception as e:
await self.module_logger.log_exception(
exception=e,
text=f"Failed to create depot {code_depot_name_to_be_created} in Gitea. Error: {e}",
properties={
"code_depot_name_to_be_created": code_depot_name_to_be_created
},
)
raise InvalidOperationError(
f"Failed to create depot {code_depot_name_to_be_created} in Gitea"
)
await new_depot_doc.set({CodeDepotDoc.depot_status: DepotStatus.CREATED})
await new_depot_doc.save()
return str(new_depot_doc.id)
def get_depot_ssh_url(self, code_depot_name: str) -> str:
"""Return the ssh url of the code depot
Parameters:
depot_name (str): the name of the depot
Returns:
str: the ssh url of the code depot
"""
if self.code_depot_ssh_port != "22":
return f"git@{self.code_depot_domain_name}:{self.code_depot_ssh_port}/{self.gitea_org}/{code_depot_name}.git"
else:
return f"git@{self.code_depot_domain_name}/{self.gitea_org}/{code_depot_name}.git"
def get_depot_http_url(self, code_depot_name: str) -> str:
"""Return the http url of the code depot
Parameters:
depot_name (str): the name of the depot
Returns:
str: the http url of the code depot
"""
if self.code_depot_http_port in ["443", "3443"]:
return f"https://{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
else:
return f"http://{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
def get_depot_http_url_with_user_name(
self, code_depot_name: str, user_name: str
) -> str:
"""Return the http url of the code depot
Parameters:
depot_name (str): the name of the depot
Returns:
str: the http url of the code depot
"""
if self.code_depot_http_port in ["443", "3443"]:
return f"https://{user_name}@{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
else:
return f"http://{user_name}@{self.code_depot_domain_name}:{self.code_depot_http_port}/{self.gitea_org}/{code_depot_name}.git"
async def get_depot_users(self, code_depot_name: str) -> list[str]:
"""Return list of user names have permission to access the depot
Parameters:
depot_name (str): the name of the depot
Returns:
list: list of user names
"""
result = await CodeDepotDoc.find_one(CodeDepotDoc.depot_name == code_depot_name)
if result:
return result.collaborators
else:
return []
async def update_depot_user_password(self, user_name: str, password: str) -> bool:
"""Update the password of the user in Gitea
Parameters:
user_name (str): the name of the user
password (str): the new password of the user
Returns:
bool: True if operations succeed, otherwise return False
"""
depot_user = self.gitea_admin.get_user_by_name(user_name)
if depot_user:
try:
self.gitea_admin.update_user_password(user_name, password)
except Exception as e:
await self.module_logger.log_exception(
exception=e,
text=f"Failed to update password for user {user_name} in Gitea.",
properties={"user_name": user_name},
)
return False
else:
await self.module_logger.log_error(
error=f"User {user_name} does not exist in Gitea.",
properties={"user_name": user_name},
)
return False
return True
async def create_depot_user(
self, user_name: str, password: str, email: str
) -> bool:
"""Create a new user in Gitea
Parameters:
user_name (str): the name of the user
password (str): the password of the user
email (str): email address of the user
Returns:
bool: True if operations succeed, otherwise return False
"""
depot_user = self.gitea_admin.get_user_by_name(user_name)
if depot_user:
await self.module_logger.log_info(
info=f"User {user_name} exist in Gitea.",
properties={"user_name": user_name},
)
return True
else:
try:
await self.module_logger.log_info(
info=f"Create user {user_name} in Gitea with password.",
properties={"user_name": user_name},
)
depot_user = self.gitea_admin.create_user(
user_name=user_name,
login_name=user_name,
password=password,
email=email,
change_pw=False,
send_notify=False,
)
except Exception as e:
await self.module_logger.log_exception(
exception=e,
text=f"Failed to create user {user_name} in Gitea.",
properties={"user_name": user_name},
)
return False
return True
async def grant_user_depot_access(
self, user_name: str, code_depot_name: str
) -> bool:
"""Grant user access to the code depot in Gitea
Parameters:
user_name (str): the name of the user
code_depot_name (str): the name of the code depot
Returns:
bool: True if operations succeed, otherwise return False
"""
try:
code_depot = self.product_org.get_repository(code_depot_name)
except Exception as e:
await self.module_logger.log_exception(
exception=e,
text=f"Failed to get depot {code_depot_name} in Gitea.",
properties={"code_depot_name": code_depot_name},
)
return False
try:
if code_depot.add_collaborator(user_name=user_name, permission="Write"):
code_depot_doc = await CodeDepotDoc.find_one(
CodeDepotDoc.depot_name == code_depot_name
)
if code_depot_doc:
if user_name not in code_depot_doc.collaborators:
code_depot_doc.collaborators.append(user_name)
await code_depot_doc.save()
except Exception as e:
await self.module_logger.log_exception(
exception=e,
text=f"Failed to grant permission for user {user_name} to access code depot {code_depot_name}.",
properties={"user_name": user_name, "code_depot_name": code_depot_name},
)
return False
return True
async def generate_statistic_result(
self, code_depot_name: str
) -> Optional[dict[str, any]]:
"""Call Gitea API and collect statistic result of the repository.
Args:
code_depot_name (str): the name of the code depot
Returns:
dict[str, any]: statistic result
"""
try:
code_depot = self.product_org.get_repository(code_depot_name)
except Exception as e:
await self.module_logger.log_exception(
exception=e,
text=f"Failed to get depot {code_depot_name} in Gitea.",
properties={"code_depot_name": code_depot_name},
)
return None
commits = code_depot.get_commits()
if len(commits) > 0:
commit_dates = []
for commit in commits:
commit_dates.append(parse(commit.created).date())
today = datetime.now(timezone.utc)
last_sunday = get_sunday(today)
# only get commits statistic of the last 20 weeks
last_20_sundays = [last_sunday - timedelta(weeks=i) for i in range(20)]
weekly_commits = {}
for date in last_20_sundays:
weekly_commits[date.strftime("%Y-%m-%d")] = 0
for commit_date in commit_dates:
commit_date_sunday = get_sunday(commit_date).strftime("%Y-%m-%d")
if commit_date_sunday in weekly_commits:
weekly_commits[commit_date_sunday] += 1
last_update = parse(commits[-1].created)
results = {
"total_commits": len(commits),
"last_commiter": commits[-1].commit["committer"]["name"],
"last_update": last_update.astimezone(pytz.UTC),
"weekly_commits": weekly_commits,
}
else:
results = {
"total_commits": 0,
"last_commiter": "",
"last_update": None,
"weekly_commits": {},
}
return results
async def analyze_code_depots(self):
code_depot_docs = await CodeDepotDoc.find(
CodeDepotDoc.depot_status == DepotStatus.CREATED
).to_list()
for code_depot_doc in code_depot_docs:
await self.module_logger.log_info(
info="Start to analyze code depot {}".format(code_depot_doc.depot_name),
properties={"code_depot_name": code_depot_doc.depot_name},
)
statistic_result = await self.generate_statistic_result(
code_depot_doc.depot_name
)
# cannot get the statistic result, probably it cannot find the
# git repository in gitea
if not statistic_result:
continue
code_depot_doc.total_commits = statistic_result["total_commits"]
code_depot_doc.last_commiter = statistic_result["last_commiter"]
code_depot_doc.last_update = statistic_result["last_update"]
code_depot_doc.weekly_commits = statistic_result["weekly_commits"]
await code_depot_doc.save()
async def fetch_code_depot(self, code_depot_id: str) -> Optional[dict[str, any]]:
code_depot_doc = await CodeDepotDoc.get(code_depot_id)
if code_depot_doc:
return code_depot_doc.model_dump()
else:
return None