Skip to content

Commit 638f984

Browse files
joaomariolagopatrickelectric
authored andcommitted
kraken: api: Move API to dedicated module
* Move Kraken API to dedicated module and add skeleton of API V2 as preparation for kraken V2 refactor
1 parent c30c4e0 commit 638f984

File tree

15 files changed

+537
-0
lines changed

15 files changed

+537
-0
lines changed

core/services/kraken/api/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from api.app import application
2+
3+
__all__ = ["application"]

core/services/kraken/api/app.py

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from os import path
2+
3+
from commonwealth.utils.apis import GenericErrorHandlingRoute
4+
from fastapi import FastAPI
5+
from fastapi.responses import RedirectResponse
6+
from fastapi.staticfiles import StaticFiles
7+
from fastapi_versioning import VersionedFastAPI
8+
9+
# Routers
10+
from api.v1.routers import extension_router_v1, index_router_v1
11+
from api.v2.routers import (
12+
container_router_v2,
13+
extension_router_v2,
14+
index_router_v2,
15+
manifest_router_v2,
16+
)
17+
18+
application = FastAPI(
19+
title="Kraken API",
20+
description="Kraken is the BlueOS service responsible for installing and managing extensions.",
21+
)
22+
application.router.route_class = GenericErrorHandlingRoute
23+
24+
# API v1
25+
application.include_router(index_router_v1)
26+
application.include_router(extension_router_v1)
27+
28+
# API v2
29+
application.include_router(index_router_v2)
30+
application.include_router(container_router_v2)
31+
application.include_router(extension_router_v2)
32+
application.include_router(manifest_router_v2)
33+
34+
application = VersionedFastAPI(application, prefix_format="/v{major}.{minor}", enable_latest=True)
35+
36+
37+
@application.get("/", status_code=200)
38+
async def root() -> RedirectResponse:
39+
"""
40+
Root endpoint for the Kraken.
41+
"""
42+
43+
return RedirectResponse(url="/static/pages/root.html")
44+
45+
46+
# Mount static files
47+
application.mount("/static", StaticFiles(directory=path.join(path.dirname(__file__), "static")), name="static")
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Kraken</title>
7+
<style>
8+
body {
9+
margin: 0;
10+
padding: 0;
11+
width: 100%;
12+
height: 100vh;
13+
display: flex;
14+
justify-content: center;
15+
align-items: center;
16+
background-color: #007bff;
17+
}
18+
.container {
19+
text-align: center;
20+
color: #fff;
21+
}
22+
img {
23+
max-width: 100%;
24+
height: auto;
25+
}
26+
h1 {
27+
font-size: 2.5rem;
28+
}
29+
ul {
30+
list-style: none;
31+
list-style-type: none;
32+
}
33+
li {
34+
font-family: Arial, Helvetica, sans-serif;
35+
font-size: 1.4rem;
36+
padding-bottom: 5px;
37+
}
38+
</style>
39+
</head>
40+
<body>
41+
<div class="container">
42+
<h1>You have found the Kraken!</h1>
43+
<img alt="" src="../assets/logo.png" />
44+
<h3>Check out Kraken's docs:</h3>
45+
<ul>
46+
<li><a href="../../v1.0/docs" style="color: #FFD700;">V1</a></li>
47+
<li><a href="../../v2.0/docs" style="color: #FFD700;">V2</a></li>
48+
</ul>
49+
</div>
50+
</body>
51+
</html>

core/services/kraken/api/v1/models.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from pydantic import BaseModel
2+
3+
4+
class Extension(BaseModel):
5+
name: str
6+
docker: str
7+
tag: str
8+
permissions: str
9+
enabled: bool
10+
identifier: str
11+
user_permissions: str
12+
13+
def is_valid(self) -> bool:
14+
return all([self.name, self.docker, self.tag, any([self.permissions, self.user_permissions]), self.identifier])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from api.v1.routers.extension import extension_router_v1
2+
from api.v1.routers.index import index_router_v1
3+
4+
__all__ = ["extension_router_v1", "index_router_v1"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from typing import Any
2+
3+
from commonwealth.utils.streaming import streamer
4+
from fastapi import APIRouter, HTTPException, status
5+
from fastapi.responses import StreamingResponse
6+
from fastapi_versioning import versioned_api_route
7+
8+
from api.v1.models import Extension
9+
from kraken import kraken_instance
10+
11+
extension_router_v1 = APIRouter(
12+
prefix="/extension",
13+
tags=["extension_v1"],
14+
route_class=versioned_api_route(1, 0),
15+
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
16+
)
17+
18+
@extension_router_v1.post("/install", status_code=status.HTTP_201_CREATED)
19+
async def install_extension(extension: Extension) -> Any:
20+
if not extension.is_valid():
21+
raise HTTPException(
22+
status_code=status.HTTP_400_BAD_REQUEST,
23+
detail="Invalid extension description",
24+
)
25+
if not kraken_instance.has_enough_disk_space():
26+
raise HTTPException(
27+
status_code=status.HTTP_507_INSUFFICIENT_STORAGE,
28+
detail="Not enough disk space to install the extension",
29+
)
30+
compatible_digest = await kraken_instance.is_compatible_extension(extension.identifier, extension.tag)
31+
# Throw an exception only if compatible_digest is False, indicating the extension is in the manifest but it is
32+
# incompatible. If compatible_digest is None, we are going to trusty that the image is compatible and will work
33+
if compatible_digest is False:
34+
raise HTTPException(
35+
status_code=status.HTTP_400_BAD_REQUEST,
36+
detail="Extension is not compatible with the current machine running BlueOS.",
37+
)
38+
return StreamingResponse(streamer(kraken_instance.install_extension(extension, compatible_digest)))
39+
40+
41+
@extension_router_v1.post("/uninstall", status_code=status.HTTP_200_OK)
42+
async def uninstall_extension(extension_identifier: str) -> Any:
43+
return await kraken_instance.uninstall_extension_from_identifier(extension_identifier)
44+
45+
46+
@extension_router_v1.post("/update_to_version", status_code=status.HTTP_201_CREATED)
47+
async def update_extension(extension_identifier: str, new_version: str) -> Any:
48+
return StreamingResponse(streamer(kraken_instance.update_extension_to_version(extension_identifier, new_version)))
49+
50+
51+
@extension_router_v1.post("/enable", status_code=status.HTTP_200_OK)
52+
async def enable_extension(extension_identifier: str) -> Any:
53+
return await kraken_instance.enable_extension(extension_identifier)
54+
55+
56+
@extension_router_v1.post("/disable", status_code=status.HTTP_200_OK)
57+
async def disable_extension(extension_identifier: str) -> Any:
58+
return await kraken_instance.disable_extension(extension_identifier)
59+
60+
61+
@extension_router_v1.post("/restart", status_code=status.HTTP_202_ACCEPTED)
62+
async def restart_extension(extension_identifier: str) -> Any:
63+
return await kraken_instance.restart_extension(extension_identifier)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from typing import Any, Iterable
2+
3+
from commonwealth.utils.streaming import timeout_streamer
4+
from fastapi import APIRouter, status
5+
from fastapi.responses import PlainTextResponse, RedirectResponse, StreamingResponse
6+
from fastapi_versioning import versioned_api_route
7+
8+
from api.v1.models import Extension
9+
from kraken import kraken_instance
10+
11+
index_router_v1 = APIRouter(
12+
tags=["index_v1"],
13+
route_class=versioned_api_route(1, 0),
14+
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
15+
)
16+
17+
@index_router_v1.get("/extensions_manifest", status_code=status.HTTP_200_OK)
18+
async def fetch_manifest() -> Any:
19+
return await kraken_instance.fetch_manifest()
20+
21+
22+
@index_router_v1.get("/installed_extensions", status_code=status.HTTP_200_OK)
23+
async def get_installed_extensions() -> Any:
24+
extensions = await kraken_instance.get_configured_extensions()
25+
extensions_list = [
26+
Extension(
27+
identifier=extension.identifier,
28+
name=extension.name,
29+
docker=extension.docker,
30+
tag=extension.tag,
31+
permissions=extension.permissions,
32+
enabled=extension.enabled,
33+
user_permissions=extension.user_permissions,
34+
)
35+
for extension in extensions
36+
]
37+
extensions_list.sort(key=lambda extension: extension.name)
38+
return extensions_list
39+
40+
41+
@index_router_v1.get("/list_containers", status_code=status.HTTP_200_OK)
42+
async def list_containers() -> Any:
43+
containers = await kraken_instance.list_containers()
44+
return [
45+
{
46+
"name": container["Names"][0],
47+
"image": container["Image"],
48+
"imageId": container["ImageID"],
49+
"status": container["Status"],
50+
}
51+
for container in containers
52+
]
53+
54+
55+
@index_router_v1.get("/log", status_code=status.HTTP_200_OK, response_class=PlainTextResponse)
56+
async def log_containers(container_name: str) -> Iterable[bytes]:
57+
return StreamingResponse(timeout_streamer(kraken_instance.stream_logs(container_name)), media_type="text/plain") # type: ignore
58+
59+
60+
@index_router_v1.get("/stats", status_code=status.HTTP_200_OK)
61+
async def load_stats() -> Any:
62+
return await kraken_instance.load_stats()
63+
64+
65+
@index_router_v1.get("/", status_code=200)
66+
async def root() -> RedirectResponse:
67+
"""
68+
Root endpoint for the Kraken API V1.
69+
"""
70+
71+
return RedirectResponse(url="/v1.0/docs")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from api.v2.routers.container import container_router_v2
2+
from api.v2.routers.extension import extension_router_v2
3+
from api.v2.routers.index import index_router_v2
4+
from api.v2.routers.manifest import manifest_router_v2
5+
6+
__all__ = ["container_router_v2", "extension_router_v2", "index_router_v2", "manifest_router_v2"]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from fastapi import APIRouter, status
2+
from fastapi.responses import Response
3+
from fastapi_versioning import versioned_api_route
4+
5+
container_router_v2 = APIRouter(
6+
prefix="/container",
7+
tags=["container_v2"],
8+
route_class=versioned_api_route(2, 0),
9+
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
10+
)
11+
12+
13+
@container_router_v2.get("/", status_code=status.HTTP_200_OK)
14+
async def list_container() -> Response:
15+
"""
16+
List details all running containers.
17+
"""
18+
return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)
19+
20+
21+
@container_router_v2.get("/{container_name}/details", status_code=status.HTTP_200_OK)
22+
async def fetch_container(_container_name: str) -> Response:
23+
"""
24+
List details of a given container.
25+
"""
26+
return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)
27+
28+
29+
@container_router_v2.get("/log", status_code=status.HTTP_200_OK)
30+
async def list_log() -> Response:
31+
"""
32+
List logs all running containers.
33+
"""
34+
return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)
35+
36+
37+
@container_router_v2.get("/{container_name}/log", status_code=status.HTTP_200_OK)
38+
async def fetch_log_by_container_name(_container_name: str) -> Response:
39+
"""
40+
Get logs of a given container.
41+
"""
42+
return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)
43+
44+
45+
@container_router_v2.get("/stats", status_code=status.HTTP_200_OK)
46+
async def list_stats() -> Response:
47+
"""
48+
List stats of all running containers.
49+
"""
50+
return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)
51+
52+
53+
@container_router_v2.get("/{container_name}/stats", status_code=status.HTTP_200_OK)
54+
async def fetch_stats_by_container_name(_container_name: str) -> Response:
55+
"""
56+
List stats of a given running containers.
57+
"""
58+
return Response(status_code=status.HTTP_501_NOT_IMPLEMENTED)

0 commit comments

Comments
 (0)