Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -477,12 +477,14 @@ export default Vue.extend({
const file = this.getInputFile()
if (file) {
this.disable_upload_controls = true
const formData = new FormData()
formData.append('file', file)

await back_axios({
method: 'POST',
url: '/version-chooser/v1.0/version/load/',
url: '/version-chooser/v1.0/version/load',
timeout: 15 * 60 * 1000, // Wait for 15min
data: file,
headers: { 'Content-Type': 'undefined' },
data: formData,
onUploadProgress: (event) => {
this.upload_percentage = Math.round(100 * (event.loaded / event.total))
},
Expand Down
4 changes: 4 additions & 0 deletions core/services/versionchooser/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# pylint: disable=W0406
from .app import application

__all__ = ["application"]
47 changes: 47 additions & 0 deletions core/services/versionchooser/api/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from os import path

from commonwealth.utils.apis import GenericErrorHandlingRoute, PrettyJSONResponse
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi_versioning import VersionedFastAPI

# Routers
from api.v1.routers import (
index_router_v1,
docker_router_v1,
version_router_v1,
bootstrap_router_v1,
)

application = FastAPI(
title="Version Chooser API",
description="Version Chooser is the BlueOS service responsible for managing BlueOS versions",
default_response_class=PrettyJSONResponse,
)
application.router.route_class = GenericErrorHandlingRoute

# API v1
application.include_router(index_router_v1)
application.include_router(docker_router_v1)
application.include_router(version_router_v1)
application.include_router(bootstrap_router_v1)


application = VersionedFastAPI(application, prefix_format="/v{major}.{minor}", enable_latest=True)


@application.get("/", status_code=200)
async def root() -> HTMLResponse:
html_content = """
<html>
<head>
<title>Version Chooser</title>
</head>
</html>
"""
return HTMLResponse(content=html_content)


# Mount static files
application.mount("/static", StaticFiles(directory=path.join(path.dirname(__file__), "static")), name="static")
Empty file.
7 changes: 7 additions & 0 deletions core/services/versionchooser/api/v1/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# pylint: disable=W0406
from .bootstrap import bootstrap_router_v1
from .index import index_router_v1
from .docker import docker_router_v1
from .version import version_router_v1

__all__ = ["bootstrap_router_v1", "index_router_v1", "docker_router_v1", "version_router_v1"]
36 changes: 36 additions & 0 deletions core/services/versionchooser/api/v1/routers/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Any

import aiodocker
from fastapi import APIRouter, Depends, status
from fastapi_versioning import versioned_api_route
from pydantic import BaseModel

from utils.chooser import VersionChooser

bootstrap_router_v1 = APIRouter(
prefix="/bootstrap",
tags=["bootstrap_v1"],
route_class=versioned_api_route(1, 0),
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
)


class BootstrapRequest(BaseModel):
tag: str


async def get_docker_client():
async with aiodocker.Docker() as docker_client:
yield VersionChooser(docker_client)


@bootstrap_router_v1.get("/current", summary="Return the current running version of BlueOS-bootstrap")
async def get_bootstrap_version(version_chooser: VersionChooser = Depends(get_docker_client)) -> Any:
return await version_chooser.get_bootstrap_version()


@bootstrap_router_v1.post("/current", summary="Sets the current version of BlueOS-bootstrap to a new tag")
async def set_bootstrap_version(
request: BootstrapRequest, version_chooser: VersionChooser = Depends(get_docker_client)
) -> Any:
return await version_chooser.set_bootstrap_version(request.tag)
34 changes: 34 additions & 0 deletions core/services/versionchooser/api/v1/routers/docker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Any

from fastapi import APIRouter, status
from fastapi_versioning import versioned_api_route


from docker_login import (
DockerLoginInfo,
get_docker_accounts,
make_docker_login,
make_docker_logout,
)

docker_router_v1 = APIRouter(
prefix="/docker",
tags=["docker_v1"],
route_class=versioned_api_route(1, 0),
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
)


@docker_router_v1.post("/login", summary="Login Docker daemon to a registry")
async def docker_login(request: DockerLoginInfo) -> None:
return make_docker_login(request)


@docker_router_v1.post("/logout", summary="Logout Docker daemon from a registry")
async def docker_logout(request: DockerLoginInfo) -> Any:
return make_docker_logout(request)


@docker_router_v1.get("/accounts", summary="Get the list of accounts logged in")
def docker_accounts() -> Any:
return get_docker_accounts()
17 changes: 17 additions & 0 deletions core/services/versionchooser/api/v1/routers/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import APIRouter, status
from fastapi.responses import RedirectResponse
from fastapi_versioning import versioned_api_route

index_router_v1 = APIRouter(
tags=["index_v1"],
route_class=versioned_api_route(1, 0),
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
)


@index_router_v1.get("/", status_code=200)
async def root() -> RedirectResponse:
"""
Root endpoint for the Version Chooser API V1.
"""
return RedirectResponse(url="/v1.0/docs")
78 changes: 78 additions & 0 deletions core/services/versionchooser/api/v1/routers/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from typing import Any

import aiodocker
from fastapi import APIRouter, Depends, File, UploadFile, status
from fastapi_versioning import versioned_api_route
from pydantic import BaseModel

from utils.chooser import VersionChooser

version_router_v1 = APIRouter(
prefix="/version",
tags=["version_v1"],
route_class=versioned_api_route(1, 0),
responses={status.HTTP_404_NOT_FOUND: {"description": "Not found"}},
)


class DockerImageIdentifier(BaseModel):
repository: str
tag: str


async def get_docker_client():
async with aiodocker.Docker() as docker_client:
yield VersionChooser(docker_client)


@version_router_v1.get(
"/current", summary="Return the current running version of BlueOS", status_code=status.HTTP_200_OK
)
async def get_version(version_chooser: VersionChooser = Depends(get_docker_client)) -> Any:
return await version_chooser.get_version()


@version_router_v1.post("/current", summary="Sets the current version of BlueOS to a new tag")
async def set_version(
request: DockerImageIdentifier, version_chooser: VersionChooser = Depends(get_docker_client)
) -> Any:
return await version_chooser.set_version(request.repository, request.tag)


@version_router_v1.post("/pull", summary="Pulls a version from dockerhub")
async def pull_version(
request: DockerImageIdentifier, version_chooser: VersionChooser = Depends(get_docker_client)
) -> Any:
return await version_chooser.pull_version(request.repository, request.tag)


@version_router_v1.delete("/delete", summary="Delete the selected version of BlueOS")
async def delete_version(
request: DockerImageIdentifier, version_chooser: VersionChooser = Depends(get_docker_client)
) -> Any:
return await version_chooser.delete_version(request.repository, request.tag)


@version_router_v1.get("/available/local", summary="Returns available local versions")
async def get_available_local_versions(version_chooser: VersionChooser = Depends(get_docker_client)) -> Any:
return await version_chooser.get_available_local_versions()


@version_router_v1.get(
"/available/{repository}/{image}", summary="Returns available versions, both locally and in dockerhub"
)
async def get_available_versions(
repository: str, image: str, version_chooser: VersionChooser = Depends(get_docker_client)
) -> Any:
return await version_chooser.get_available_versions(f"{repository}/{image}")


@version_router_v1.post("/load", summary="Load a docker tar file")
async def load(file: UploadFile = File(...), version_chooser: VersionChooser = Depends(get_docker_client)) -> Any:
data = await file.read()
return await version_chooser.load(data)


@version_router_v1.post("/restart", summary="Restart the currently running docker container")
async def restart(version_chooser: VersionChooser = Depends(get_docker_client)) -> Any:
return await version_chooser.restart()
31 changes: 31 additions & 0 deletions core/services/versionchooser/args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import argparse
from dataclasses import dataclass


@dataclass
class CommandLineArgs:
"""
Represents command-line arguments for Version Chooser.

Attributes:
debug (bool): Enable debug mode
host (str): Host to server version-chooser on
port (int): Port to server version-chooser on
"""

debug: bool
host: str
port: int

@staticmethod
def from_args() -> "CommandLineArgs":
parser = argparse.ArgumentParser(description="Version Chooser Manager service.")

parser.add_argument("--debug", action="store_true", default=False, help="Enable debug mode")
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to server version-chooser on")
parser.add_argument("--port", type=int, default=8081, help="Port to server version-chooser on")

args = parser.parse_args()
client_args = CommandLineArgs(debug=args.debug, host=args.host, port=args.port)

return client_args
25 changes: 8 additions & 17 deletions core/services/versionchooser/docker_login.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import base64
import json
import os
from dataclasses import asdict, dataclass
from typing import Any, Dict, List
from typing import Dict, List

from aiohttp import web
from commonwealth.utils.apis import PrettyJSONResponse
from pydantic import BaseModel

DEFAULT_DOCKER_REGISTRY = "https://index.docker.io/v1/"
DOCKER_USER_CONFIG_DIR = "/home/pi/.docker"
DOCKER_ROOT_CONFIG_DIR = "/root/.docker"

Expand All @@ -15,21 +16,11 @@
DEFAULT_DOCKER_REGISTRY = "https://index.docker.io/v1/"


@dataclass
class DockerLoginInfo:
class DockerLoginInfo(BaseModel):
root: bool = True
registry: str = DEFAULT_DOCKER_REGISTRY
username: str = ""
password: str = ""

@staticmethod
def from_json(data: Dict[str, Any]) -> "DockerLoginInfo":
return DockerLoginInfo(
root=data.get("root", True),
registry=data.get("registry", DEFAULT_DOCKER_REGISTRY),
username=data.get("username", ""),
password=data.get("password", ""),
)
registry: str = DEFAULT_DOCKER_REGISTRY


def get_accounts_from_file(file_path: str, root: bool) -> List[DockerLoginInfo]:
Expand Down Expand Up @@ -95,7 +86,7 @@ def logout_from_file(info: DockerLoginInfo, file_path: str) -> None:
json.dump(config, file, indent=4)


def get_docker_accounts() -> web.Response:
def get_docker_accounts() -> PrettyJSONResponse:
root_accounts = get_accounts_from_file(DOCKER_ROOT_CONFIG_FILE, True)
user_accounts = get_accounts_from_file(DOCKER_USER_CONFIG_FILE, False)

Expand All @@ -105,7 +96,7 @@ def get_docker_accounts() -> web.Response:

accounts = root_accounts + user_accounts

return web.json_response([asdict(account) for account in accounts])
return PrettyJSONResponse(content=[account.dict() for account in accounts])


def make_docker_login(info: DockerLoginInfo) -> None:
Expand Down
Loading