|  | 
|  | 1 | +import json | 
|  | 2 | +import logging | 
|  | 3 | +import os | 
|  | 4 | +from typing import Any | 
|  | 5 | + | 
|  | 6 | +import requests | 
|  | 7 | +from requests import Response | 
|  | 8 | +from requests.auth import HTTPBasicAuth | 
|  | 9 | + | 
|  | 10 | +logger = logging.getLogger("mirror_manager") | 
|  | 11 | +logging.basicConfig(level=logging.INFO) | 
|  | 12 | + | 
|  | 13 | +DEFAULT_TIMEOUT: int = 5 * 60 | 
|  | 14 | + | 
|  | 15 | +# Mirror server configuration | 
|  | 16 | + | 
|  | 17 | +MIRROR_SERVER_TOKEN_NAME: str = "mirror_api_token" | 
|  | 18 | +MIRROR_SERVER_URL: str = os.environ["MIRROR_SERVER_URL"] | 
|  | 19 | +MIRROR_SERVER_USERNAME: str = os.environ["MIRROR_SERVER_USERNAME"] | 
|  | 20 | +MIRROR_SERVER_PASSWORD: str = os.environ["MIRROR_SERVER_PASSWORD"] | 
|  | 21 | + | 
|  | 22 | +# Workspace server configuration | 
|  | 23 | +WORKSPACE_SERVER_TOKEN_NAME: str = "push_mirror_api_token" | 
|  | 24 | +WORKSPACE_SERVER_URL: str = os.environ["WORKSPACE_SERVER_URL"] | 
|  | 25 | +WORKSPACE_SERVER_USERNAME: str = os.environ["WORKSPACE_SERVER_USERNAME"] | 
|  | 26 | +WORKSPACE_SERVER_PASSWORD: str = os.environ["WORKSPACE_SERVER_PASSWORD"] | 
|  | 27 | + | 
|  | 28 | +REPOSITORY_DATA: dict[str, list[dict[str, str]]] = json.loads( | 
|  | 29 | +    os.environ["REPOSITORY_DATA"] | 
|  | 30 | +) | 
|  | 31 | + | 
|  | 32 | + | 
|  | 33 | +def delete_token(username: str, password: str, token_name: str, gitea_url: str) -> None: | 
|  | 34 | +    headers: dict[str, str] = {"Content-Type": "application/json"} | 
|  | 35 | +    basic_auth = HTTPBasicAuth(username, password) | 
|  | 36 | + | 
|  | 37 | +    response: Response = requests.delete( | 
|  | 38 | +        f"{gitea_url}/api/v1/users/{username}/tokens/{token_name}", | 
|  | 39 | +        auth=basic_auth, | 
|  | 40 | +        headers=headers, | 
|  | 41 | +        timeout=DEFAULT_TIMEOUT, | 
|  | 42 | +    ) | 
|  | 43 | + | 
|  | 44 | +    if not response.status_code == requests.codes.no_content: | 
|  | 45 | +        logging.info( | 
|  | 46 | +            f"Cannot delete token {token_name} for user {username}." | 
|  | 47 | +            f" Status code: {response.status_code}" | 
|  | 48 | +        ) | 
|  | 49 | +    else: | 
|  | 50 | +        logging.info( | 
|  | 51 | +            f"Token {token_name} for user {username} deleted." | 
|  | 52 | +            f" Status code: {response.status_code}" | 
|  | 53 | +        ) | 
|  | 54 | + | 
|  | 55 | + | 
|  | 56 | +def create_token( | 
|  | 57 | +    username: str, password: str, token_name: str, gitea_url: str, scopes: list[str] | 
|  | 58 | +) -> Any: | 
|  | 59 | +    logger.info(f"Creating API token {token_name} for user {username}") | 
|  | 60 | + | 
|  | 61 | +    headers: dict[str, str] = {"Content-Type": "application/json"} | 
|  | 62 | +    basic_auth = HTTPBasicAuth(username, password) | 
|  | 63 | +    data: dict[str, str | list[str]] = { | 
|  | 64 | +        "name": token_name, | 
|  | 65 | +        "scopes": scopes, | 
|  | 66 | +    } | 
|  | 67 | + | 
|  | 68 | +    response: Response = requests.post( | 
|  | 69 | +        f"{gitea_url}/api/v1/users/{username}/tokens", | 
|  | 70 | +        auth=basic_auth, | 
|  | 71 | +        headers=headers, | 
|  | 72 | +        data=json.dumps(data), | 
|  | 73 | +        timeout=DEFAULT_TIMEOUT, | 
|  | 74 | +    ) | 
|  | 75 | + | 
|  | 76 | +    if not response.status_code == requests.codes.created: | 
|  | 77 | +        error_message: str = ( | 
|  | 78 | +            f"Cannot create tokens for user {username}." | 
|  | 79 | +            f" Status code: {response.status_code}." | 
|  | 80 | +            f" Response {response.json()}" | 
|  | 81 | +        ) | 
|  | 82 | + | 
|  | 83 | +        raise Exception(error_message) | 
|  | 84 | + | 
|  | 85 | +    return response.json()["sha1"] | 
|  | 86 | + | 
|  | 87 | + | 
|  | 88 | +def create_migration( | 
|  | 89 | +    repository_url: str, | 
|  | 90 | +    repository_name: str, | 
|  | 91 | +    repository_auth_token: str, | 
|  | 92 | +    gitea_url: str, | 
|  | 93 | +    token: str, | 
|  | 94 | +    service: str, | 
|  | 95 | +) -> tuple[Any, Any]: | 
|  | 96 | +    logger.info(f"Creating a migration for repository {repository_url}") | 
|  | 97 | + | 
|  | 98 | +    headers: dict[str, str] = {"Content-Type": "application/json"} | 
|  | 99 | +    params: dict[str, str] = {"access_token": token} | 
|  | 100 | + | 
|  | 101 | +    data: dict[str, str | bool] = { | 
|  | 102 | +        "clone_addr": repository_url, | 
|  | 103 | +        "auth_token": repository_auth_token, | 
|  | 104 | +        "mirror": True, | 
|  | 105 | +        "mirror_interval": "0h10m0s", | 
|  | 106 | +        "private": False, | 
|  | 107 | +        "repo_name": repository_name, | 
|  | 108 | +        "service": service, | 
|  | 109 | +    } | 
|  | 110 | + | 
|  | 111 | +    response: Response = requests.post( | 
|  | 112 | +        f"{gitea_url}/api/v1/repos/migrate", | 
|  | 113 | +        params=params, | 
|  | 114 | +        headers=headers, | 
|  | 115 | +        data=json.dumps(data), | 
|  | 116 | +        timeout=DEFAULT_TIMEOUT, | 
|  | 117 | +    ) | 
|  | 118 | + | 
|  | 119 | +    if not response.status_code == requests.codes.created: | 
|  | 120 | +        error_message: str = ( | 
|  | 121 | +            f"Cannot create migration for repository {repository_url}." | 
|  | 122 | +            f" Status code: {response.status_code}. Response {response.json()}" | 
|  | 123 | +        ) | 
|  | 124 | + | 
|  | 125 | +        raise Exception(error_message) | 
|  | 126 | + | 
|  | 127 | +    logger.info( | 
|  | 128 | +        f"Migration created for user {response.json()['owner']['username']}" | 
|  | 129 | +        f" at repository {response.json()['name']}" | 
|  | 130 | +    ) | 
|  | 131 | +    return response.json()["owner"]["username"], response.json()["name"] | 
|  | 132 | + | 
|  | 133 | + | 
|  | 134 | +def delete_repository( | 
|  | 135 | +    username: str, gitea_url: str, repository_name: str, token: str | 
|  | 136 | +) -> None: | 
|  | 137 | +    logger.info( | 
|  | 138 | +        f"Attempting to delete repository {repository_name} for user {username}" | 
|  | 139 | +    ) | 
|  | 140 | + | 
|  | 141 | +    headers: dict[str, str] = {"Content-Type": "application/json"} | 
|  | 142 | +    params: dict[str, str] = {"access_token": token} | 
|  | 143 | + | 
|  | 144 | +    response: Response = requests.delete( | 
|  | 145 | +        f"{gitea_url}/api/v1/repos/{username}/{repository_name}", | 
|  | 146 | +        params=params, | 
|  | 147 | +        headers=headers, | 
|  | 148 | +        timeout=DEFAULT_TIMEOUT, | 
|  | 149 | +    ) | 
|  | 150 | + | 
|  | 151 | +    if response.status_code == requests.codes.no_content: | 
|  | 152 | +        logging.info("Repository successfully deleted.") | 
|  | 153 | +    else: | 
|  | 154 | +        logging.info(f"Cannot delete repository. Response {response.json()}") | 
|  | 155 | + | 
|  | 156 | + | 
|  | 157 | +def obtain_api_token( | 
|  | 158 | +    token_name: str, username: str, password: str, scopes: list[str], gitea_url: str | 
|  | 159 | +) -> Any: | 
|  | 160 | +    delete_token( | 
|  | 161 | +        username=username, password=password, token_name=token_name, gitea_url=gitea_url | 
|  | 162 | +    ) | 
|  | 163 | + | 
|  | 164 | +    token_value = create_token( | 
|  | 165 | +        username=username, | 
|  | 166 | +        password=password, | 
|  | 167 | +        token_name=token_name, | 
|  | 168 | +        scopes=scopes, | 
|  | 169 | +        gitea_url=gitea_url, | 
|  | 170 | +    ) | 
|  | 171 | + | 
|  | 172 | +    return token_value | 
|  | 173 | + | 
|  | 174 | + | 
|  | 175 | +def get_repositories(owner: str, gitea_url: str, token: str) -> list[str]: | 
|  | 176 | +    logger.info(f"Searching for repositories of {owner} at {gitea_url}") | 
|  | 177 | + | 
|  | 178 | +    headers: dict[str, str] = {"Content-Type": "application/json"} | 
|  | 179 | +    params: dict[str, str] = {"access_token": token} | 
|  | 180 | + | 
|  | 181 | +    response: Response = requests.get( | 
|  | 182 | +        f"{gitea_url}/api/v1/repos/search", | 
|  | 183 | +        params=params, | 
|  | 184 | +        headers=headers, | 
|  | 185 | +        timeout=DEFAULT_TIMEOUT, | 
|  | 186 | +    ) | 
|  | 187 | + | 
|  | 188 | +    if not response.status_code == requests.codes.ok: | 
|  | 189 | +        error_message: str = ( | 
|  | 190 | +            f"Could not list repositories for user {owner}. " | 
|  | 191 | +            f"Status code: {response.status_code}. Response {response.json()}" | 
|  | 192 | +        ) | 
|  | 193 | + | 
|  | 194 | +        raise Exception(error_message) | 
|  | 195 | + | 
|  | 196 | +    return [ | 
|  | 197 | +        repository["name"] | 
|  | 198 | +        for repository in response.json()["data"] | 
|  | 199 | +        if repository["owner"]["username"] == owner | 
|  | 200 | +    ] | 
|  | 201 | + | 
|  | 202 | + | 
|  | 203 | +def main() -> None: | 
|  | 204 | +    gitea_mirror_token = obtain_api_token( | 
|  | 205 | +        token_name=MIRROR_SERVER_TOKEN_NAME, | 
|  | 206 | +        username=MIRROR_SERVER_USERNAME, | 
|  | 207 | +        password=MIRROR_SERVER_PASSWORD, | 
|  | 208 | +        scopes=["write:repository"], | 
|  | 209 | +        gitea_url=MIRROR_SERVER_URL, | 
|  | 210 | +    ) | 
|  | 211 | + | 
|  | 212 | +    workspace_gitea_token = obtain_api_token( | 
|  | 213 | +        token_name=WORKSPACE_SERVER_TOKEN_NAME, | 
|  | 214 | +        username=WORKSPACE_SERVER_USERNAME, | 
|  | 215 | +        password=WORKSPACE_SERVER_PASSWORD, | 
|  | 216 | +        gitea_url=WORKSPACE_SERVER_URL, | 
|  | 217 | +        scopes=["write:repository", "write:user"], | 
|  | 218 | +    ) | 
|  | 219 | + | 
|  | 220 | +    for repository_name in get_repositories( | 
|  | 221 | +        owner=MIRROR_SERVER_USERNAME, | 
|  | 222 | +        gitea_url=MIRROR_SERVER_URL, | 
|  | 223 | +        token=gitea_mirror_token, | 
|  | 224 | +    ): | 
|  | 225 | +        delete_repository( | 
|  | 226 | +            username=MIRROR_SERVER_USERNAME, | 
|  | 227 | +            gitea_url=MIRROR_SERVER_URL, | 
|  | 228 | +            repository_name=repository_name, | 
|  | 229 | +            token=gitea_mirror_token, | 
|  | 230 | +        ) | 
|  | 231 | + | 
|  | 232 | +    for repository_name in get_repositories( | 
|  | 233 | +        owner=WORKSPACE_SERVER_USERNAME, | 
|  | 234 | +        gitea_url=WORKSPACE_SERVER_URL, | 
|  | 235 | +        token=workspace_gitea_token, | 
|  | 236 | +    ): | 
|  | 237 | +        delete_repository( | 
|  | 238 | +            username=WORKSPACE_SERVER_USERNAME, | 
|  | 239 | +            gitea_url=WORKSPACE_SERVER_URL, | 
|  | 240 | +            repository_name=repository_name, | 
|  | 241 | +            token=workspace_gitea_token, | 
|  | 242 | +        ) | 
|  | 243 | + | 
|  | 244 | +    for repository in REPOSITORY_DATA["repositories"]: | 
|  | 245 | + | 
|  | 246 | +        owner, repository_name = create_migration( | 
|  | 247 | +            repository_name=repository["repository_name"], | 
|  | 248 | +            repository_url=repository["repository_url"], | 
|  | 249 | +            repository_auth_token=repository["repository_auth_token"], | 
|  | 250 | +            gitea_url=MIRROR_SERVER_URL, | 
|  | 251 | +            token=gitea_mirror_token, | 
|  | 252 | +            service="github",  # TODO(cgavidia): Maybe this can be a parameter in config. | 
|  | 253 | +        ) | 
|  | 254 | + | 
|  | 255 | +        create_migration( | 
|  | 256 | +            repository_name=f"{repository['repository_name']}-mirror", | 
|  | 257 | +            repository_url=f"{MIRROR_SERVER_URL}/{owner}/{repository_name}", | 
|  | 258 | +            repository_auth_token=gitea_mirror_token, | 
|  | 259 | +            gitea_url=WORKSPACE_SERVER_URL, | 
|  | 260 | +            token=workspace_gitea_token, | 
|  | 261 | +            service="gitea", | 
|  | 262 | +        ) | 
|  | 263 | + | 
|  | 264 | + | 
|  | 265 | +if __name__ == "__main__": | 
|  | 266 | +    main() | 
0 commit comments