From d1dc9cd80943981da4c6b0981022dfba5a548d01 Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Tue, 27 Oct 2020 14:04:00 -0700 Subject: [PATCH 01/26] added basic docker-compose functionality to api (no-auth) --- backend/api/actions/apps.py | 16 ++- backend/api/actions/compose.py | 53 ++++++++ backend/api/main.py | 7 +- backend/api/routers/compose.py | 14 ++ backend/api/settings.py | 3 +- backend/api/utils/__init__.py | 5 + backend/api/{utils.py => utils/apps.py} | 124 +----------------- backend/api/utils/auth.py | 28 ++++ backend/api/utils/compose.py | 48 +++++++ backend/api/utils/resources.py | 0 backend/api/utils/templates.py | 89 +++++++++++++ .../applications/ApplicationDetails.vue | 2 +- 12 files changed, 263 insertions(+), 126 deletions(-) create mode 100644 backend/api/actions/compose.py create mode 100644 backend/api/routers/compose.py create mode 100644 backend/api/utils/__init__.py rename backend/api/{utils.py => utils/apps.py} (70%) create mode 100644 backend/api/utils/auth.py create mode 100644 backend/api/utils/compose.py create mode 100644 backend/api/utils/resources.py create mode 100644 backend/api/utils/templates.py diff --git a/backend/api/actions/apps.py b/backend/api/actions/apps.py index 394647ec..69de4231 100644 --- a/backend/api/actions/apps.py +++ b/backend/api/actions/apps.py @@ -4,6 +4,7 @@ from ..db import models, schemas from ..utils import * from ..utils import check_updates as _update_check +from docker.errors import APIError from datetime import datetime import time @@ -44,7 +45,12 @@ def check_app_update(app_name): def get_apps(): apps_list = [] dclient = docker.from_env() - apps = dclient.containers.list(all=True) + try: + apps = dclient.containers.list(all=True) + except Exception as exc: + raise HTTPException( + status_code=exc.response.status_code, detail=exc.explanation + ) for app in apps: attrs = app.attrs @@ -191,12 +197,16 @@ def app_action(app_name, action): try: _action(force=True) except Exception as exc: - err = f"{exc}" + raise HTTPException( + status_code=exc.response.status_code, detail=exc.explanation + ) else: try: _action() except Exception as exc: - err = exc.explination + raise HTTPException( + status_code=exc.response.status_code, detail=exc.explanation + ) apps_list = get_apps() return apps_list diff --git a/backend/api/actions/compose.py b/backend/api/actions/compose.py new file mode 100644 index 00000000..b70f4b5a --- /dev/null +++ b/backend/api/actions/compose.py @@ -0,0 +1,53 @@ +from fastapi import HTTPException +from sh import docker_compose +import yaml + +from ..settings import Settings +from ..utils.compose import find_yml_files, get_readme_file, get_logo_file + +settings=Settings() + +def compose_action(name, action): + files = find_yml_files(settings.COMPOSE_DIR) + for project, file in files.items(): + if name == project: + path = file + if action == "up": + try: + _action = docker_compose("-f", path, action, '-d') + except Exception as exc: + raise HTTPException(400, exc.stderr.decode('UTF-8').rstrip()) + else: + _action = docker_compose("-f", path, action) + break + else: + raise HTTPException(404, 'Project not found.') + if _action.stdout.decode('UTF-8').rstrip(): + output = _action.stdout.decode('UTF-8').rstrip() + elif _action.stderr.decode('UTF-8').rstrip(): + output = _action.stderr.decode('UTF-8').rstrip() + else: + output = 'No Output' + return {'success': True, 'project': project, 'action': action, 'output': output} + +def get_compose_projects(): + files = find_yml_files(settings.COMPOSE_DIR) + + projects = [] + for project, file in files.items(): + volumes = [] + networks = [] + services = {} + compose = open(file) + loaded_compose = yaml.load(compose, Loader=yaml.SafeLoader) + if loaded_compose.get('volumes'): + for volume in loaded_compose.get('volumes'): + volumes.append(volume) + if loaded_compose.get('networks'): + for network in loaded_compose.get('networks'): + networks.append(network) + for service in loaded_compose.get('services'): + services[service] = loaded_compose['services'][service] + _project = {'name': project, 'path': file, 'version': loaded_compose['version'], 'services': services, 'volumes': volumes, "networks": networks} + projects.append(_project) + return projects \ No newline at end of file diff --git a/backend/api/main.py b/backend/api/main.py index bfc09f60..b66274b6 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -1,6 +1,6 @@ import uvicorn from fastapi import Depends, FastAPI, Header, HTTPException -from .routers import apps, templates, app_settings, resources, auth, user +from .routers import apps, templates, app_settings, resources, auth, user, compose import uuid from .db import models @@ -65,6 +65,11 @@ tags=["templates"], responses={404: {"description": "Not found"}}, ) +app.include_router( + compose.router, + prefix="/compose", + tags=["compose"] +) app.include_router(app_settings.router, prefix="/settings", tags=["settings"]) diff --git a/backend/api/routers/compose.py b/backend/api/routers/compose.py new file mode 100644 index 00000000..cb44bc9d --- /dev/null +++ b/backend/api/routers/compose.py @@ -0,0 +1,14 @@ +from fastapi import APIRouter, Depends, HTTPException +from typing import List +from ..auth import get_active_user +from ..actions.compose import get_compose_projects, compose_action + +router = APIRouter() + +@router.get("/") +def get_images(): + return get_compose_projects() + +@router.get("/{project_name}/{action}") +def actions(project_name, action): + return compose_action(project_name, action) \ No newline at end of file diff --git a/backend/api/settings.py b/backend/api/settings.py index 303c9c6a..e2e1dee4 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -34,4 +34,5 @@ class Settings(BaseSettings): ] SQLALCHEMY_DATABASE_URI = os.environ.get( "DATABASE_URL", "sqlite:///config/data.sqlite" - ) \ No newline at end of file + ) + COMPOSE_DIR = os.environ.get("COMPOSE_DIR", "config/compose/") \ No newline at end of file diff --git a/backend/api/utils/__init__.py b/backend/api/utils/__init__.py new file mode 100644 index 00000000..2896a566 --- /dev/null +++ b/backend/api/utils/__init__.py @@ -0,0 +1,5 @@ +from .apps import * +from .auth import * +from .compose import * +from .resources import * +from .templates import * \ No newline at end of file diff --git a/backend/api/utils.py b/backend/api/utils/apps.py similarity index 70% rename from backend/api/utils.py rename to backend/api/utils/apps.py index efa29670..c94b60e6 100644 --- a/backend/api/utils.py +++ b/backend/api/utils/apps.py @@ -1,14 +1,6 @@ -from .db import models -from .db.database import SessionLocal -import re -from typing import Dict, List, Optional - -from jose import jwt -from fastapi import Cookie, Depends, WebSocket, status, HTTPException -from fastapi.security import APIKeyCookie -from .auth import cookie_authentication -from .auth import user_db -from .settings import Settings +from ..db import models +from ..db.database import SessionLocal +from ..settings import Settings import aiodocker import docker from docker.errors import APIError @@ -16,102 +8,6 @@ settings = Settings() - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -# For Templates -REGEXP_PORT_ASSIGN = r"^(?:(?:\d{1,5}:)?\d{1,5}|:\d{1,5})/(?:tcp|udp)$" - -# Input Format: -# [ -# '80:8080/tcp', -# '123:123/udp' -# '4040/tcp', -# ] -# Result Format: -# [ -# { -# 'cport': '80', -# 'hport': '8080', -# 'proto': 'tcp', -# }, -# ... -# ] - - -def conv_ports2dict(data: List[str]) -> List[Dict[str, str]]: - if len(data) > 0 and type(data[0]) == dict: - delim = ":" - portlst = [] - for port_data in data: - for label, port in port_data.items(): - if not re.match(REGEXP_PORT_ASSIGN, port, flags=re.IGNORECASE): - raise HTTPException( - status_code=500, detail="Malformed port assignment." + port_data - ) - - hport, cport = None, port - if delim in cport: - hport, cport = cport.split(delim, 1) - if not hport: - hport = None - cport, proto = cport.split("/", 1) - portlst.append( - {"cport": cport, "hport": hport, "proto": proto, "label": label} - ) - return portlst - - elif type(data) == list: - delim = ":" - portlst = [] - for port_data in data: - if not re.match(REGEXP_PORT_ASSIGN, port_data, flags=re.IGNORECASE): - raise HTTPException( - status_code=500, detail="Malformed port assignment." + port_data - ) - - hport, cport = None, port_data - if delim in cport: - hport, cport = cport.split(delim, 1) - if not hport: - hport = None - cport, proto = cport.split("/", 1) - portlst.append({"cport": cport, "hport": hport, "proto": proto}) - return portlst - else: - return None - - -# Input Format: -# [ -# { -# 'net.ipv6.conf.all.disable_ipv6': '0' -# } -# ] -# Result Format: -# [ -# { -# 'name': 'net.ipv6.conf.all.disable_ipv6', -# 'value': '0' -# } -# ] - - -def conv_sysctls2dict(data: List[Dict[str, str]]) -> List[Dict[str, str]]: - return [{"name": k, "value": v} for item in data for k, v in item.items()] - - -def conv2dict(name, value): - _tmp_attr = {name: value} - return _tmp_attr - - # For Deploy Form # Input Format: @@ -262,19 +158,7 @@ def conv_restart2data(data): return restart -async def websocket_auth(websocket: WebSocket): - try: - cookie = websocket._cookies["fastapiusersauth"] - user = await cookie_authentication(cookie, user_db) - if user and user.is_active: - return user - elif settings.DISABLE_AUTH == "True": - return True - except: - if settings.DISABLE_AUTH == "True": - return True - else: - return None + async def calculate_cpu_percent(d): diff --git a/backend/api/utils/auth.py b/backend/api/utils/auth.py new file mode 100644 index 00000000..327975b1 --- /dev/null +++ b/backend/api/utils/auth.py @@ -0,0 +1,28 @@ +from ..auth import cookie_authentication +from ..auth import user_db +from ..settings import Settings +from ..db.database import SessionLocal +from fastapi import WebSocket + +settings = Settings() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +async def websocket_auth(websocket: WebSocket): + try: + cookie = websocket._cookies["fastapiusersauth"] + user = await cookie_authentication(cookie, user_db) + if user and user.is_active: + return user + elif settings.DISABLE_AUTH == "True": + return True + except: + if settings.DISABLE_AUTH == "True": + return True + else: + return None \ No newline at end of file diff --git a/backend/api/utils/compose.py b/backend/api/utils/compose.py new file mode 100644 index 00000000..8037774f --- /dev/null +++ b/backend/api/utils/compose.py @@ -0,0 +1,48 @@ +from ..settings import Settings +import os +import fnmatch + +settings = Settings() + +def find_yml_files(path): + """ + find docker-compose.yml files in path + """ + matches = {} + for root, _, filenames in os.walk(path, followlinks=True): + for _ in set().union(fnmatch.filter(filenames, 'docker-compose.yml'), fnmatch.filter(filenames, 'docker-compose.yaml')): + key = root.split('/')[-1] + matches[key] = os.path.join(os.getcwd(), root + '/'+_) + return matches + +def get_readme_file(path): + """ + find case insensitive readme.md in path and return the contents + """ + + readme = None + + for file in os.listdir(path): + if file.lower() == "readme.md" and os.path.isfile(os.path.join(path, file)): + file = open(os.path.join(path, file)) + readme = file.read() + file.close() + break + + return readme + +def get_logo_file(path): + """ + find case insensitive logo.png in path and return the contents + """ + + logo = None + + for file in os.listdir(path): + if file.lower() == "logo.png" and os.path.isfile(os.path.join(path, file)): + file = open(os.path.join(path, file)) + logo = file.read() + file.close() + break + + return logo \ No newline at end of file diff --git a/backend/api/utils/resources.py b/backend/api/utils/resources.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/utils/templates.py b/backend/api/utils/templates.py new file mode 100644 index 00000000..111d8eca --- /dev/null +++ b/backend/api/utils/templates.py @@ -0,0 +1,89 @@ +import re +from fastapi import HTTPException +from typing import Dict, List, Optional + +# For Templates +REGEXP_PORT_ASSIGN = r"^(?:(?:\d{1,5}:)?\d{1,5}|:\d{1,5})/(?:tcp|udp)$" + +# Input Format: +# [ +# '80:8080/tcp', +# '123:123/udp' +# '4040/tcp', +# ] +# Result Format: +# [ +# { +# 'cport': '80', +# 'hport': '8080', +# 'proto': 'tcp', +# }, +# ... +# ] + + +def conv_ports2dict(data: List[str]) -> List[Dict[str, str]]: + if len(data) > 0 and type(data[0]) == dict: + delim = ":" + portlst = [] + for port_data in data: + for label, port in port_data.items(): + if not re.match(REGEXP_PORT_ASSIGN, port, flags=re.IGNORECASE): + raise HTTPException( + status_code=500, detail="Malformed port assignment." + port_data + ) + + hport, cport = None, port + if delim in cport: + hport, cport = cport.split(delim, 1) + if not hport: + hport = None + cport, proto = cport.split("/", 1) + portlst.append( + {"cport": cport, "hport": hport, "proto": proto, "label": label} + ) + return portlst + + elif type(data) == list: + delim = ":" + portlst = [] + for port_data in data: + if not re.match(REGEXP_PORT_ASSIGN, port_data, flags=re.IGNORECASE): + raise HTTPException( + status_code=500, detail="Malformed port assignment." + port_data + ) + + hport, cport = None, port_data + if delim in cport: + hport, cport = cport.split(delim, 1) + if not hport: + hport = None + cport, proto = cport.split("/", 1) + portlst.append({"cport": cport, "hport": hport, "proto": proto}) + return portlst + else: + return None + + +# Input Format: +# [ +# { +# 'net.ipv6.conf.all.disable_ipv6': '0' +# } +# ] +# Result Format: +# [ +# { +# 'name': 'net.ipv6.conf.all.disable_ipv6', +# 'value': '0' +# } +# ] + + +def conv_sysctls2dict(data: List[Dict[str, str]]) -> List[Dict[str, str]]: + return [{"name": k, "value": v} for item in data for k, v in item.items()] + + +def conv2dict(name, value): + _tmp_attr = {name: value} + return _tmp_attr \ No newline at end of file diff --git a/frontend/src/components/applications/ApplicationDetails.vue b/frontend/src/components/applications/ApplicationDetails.vue index 43315477..8e965a37 100644 --- a/frontend/src/components/applications/ApplicationDetails.vue +++ b/frontend/src/components/applications/ApplicationDetails.vue @@ -48,7 +48,7 @@ Kill mdi-delete From 491281d9b7bb77e89e0fe33537d1c5c229a7998c Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Tue, 27 Oct 2020 15:47:20 -0700 Subject: [PATCH 02/26] fixed requirements; cd into directory to run docker compose and then back --- backend/api/actions/__init__.py | 4 +- backend/api/actions/compose.py | 67 +++++++++++++++++++++++------ backend/api/routers/app_settings.py | 2 +- backend/api/utils/compose.py | 3 +- backend/requirements.txt | 4 +- 5 files changed, 62 insertions(+), 18 deletions(-) diff --git a/backend/api/actions/__init__.py b/backend/api/actions/__init__.py index 2dbf1c6c..1f81d259 100644 --- a/backend/api/actions/__init__.py +++ b/backend/api/actions/__init__.py @@ -1 +1,3 @@ -from .apps import * \ No newline at end of file +from .apps import * +from .compose import * +from .resources import * \ No newline at end of file diff --git a/backend/api/actions/compose.py b/backend/api/actions/compose.py index b70f4b5a..8eb3716f 100644 --- a/backend/api/actions/compose.py +++ b/backend/api/actions/compose.py @@ -1,6 +1,8 @@ from fastapi import HTTPException from sh import docker_compose +import os import yaml +import pathlib from ..settings import Settings from ..utils.compose import find_yml_files, get_readme_file, get_logo_file @@ -9,26 +11,26 @@ def compose_action(name, action): files = find_yml_files(settings.COMPOSE_DIR) - for project, file in files.items(): - if name == project: - path = file - if action == "up": - try: - _action = docker_compose("-f", path, action, '-d') - except Exception as exc: - raise HTTPException(400, exc.stderr.decode('UTF-8').rstrip()) - else: - _action = docker_compose("-f", path, action) - break + compose = get_compose(name) + if action == "up": + try: + cwd = os.getcwd() + os.chdir(os.path.dirname(compose['path'])) + _action = docker_compose("-f", compose['path'], action, '-d') + os.chdir(cwd) + except Exception as exc: + os.chdir(cwd) + raise HTTPException(400, exc.stderr.decode('UTF-8').rstrip()) + else: - raise HTTPException(404, 'Project not found.') + _action = docker_compose("-f", compose['path'], action) if _action.stdout.decode('UTF-8').rstrip(): output = _action.stdout.decode('UTF-8').rstrip() elif _action.stderr.decode('UTF-8').rstrip(): output = _action.stderr.decode('UTF-8').rstrip() else: output = 'No Output' - return {'success': True, 'project': project, 'action': action, 'output': output} + return {'success': True, 'project': compose['name'], 'action': action, 'output': output} def get_compose_projects(): files = find_yml_files(settings.COMPOSE_DIR) @@ -50,4 +52,41 @@ def get_compose_projects(): services[service] = loaded_compose['services'][service] _project = {'name': project, 'path': file, 'version': loaded_compose['version'], 'services': services, 'volumes': volumes, "networks": networks} projects.append(_project) - return projects \ No newline at end of file + return projects + +def get_compose(name): + try: + files = find_yml_files(settings.COMPOSE_DIR + name) + except Exception as exc: + print(exc) + for project, file in files.items(): + if name == project: + networks = [] + volumes = [] + services = {} + compose = open(file) + loaded_compose = yaml.load(compose, Loader=yaml.SafeLoader) + if loaded_compose.get('volumes'): + for volume in loaded_compose.get('volumes'): + volumes.append(volume) + if loaded_compose.get('networks'): + for network in loaded_compose.get('networks'): + networks.append(network) + for service in loaded_compose.get('services'): + services[service] = loaded_compose['services'][service] + compose_object = {'name': project, 'path': file, 'version': loaded_compose['version'], 'services': services, 'volumes': volumes, "networks": networks} + return compose_object + break + else: + raise HTTPException(404, 'Project '+name+' not found' ) + + + +def write_compose(compose): + print(compose) + pathlib.Path("config/compose/"+compose.name).mkdir(parents=True) + f = open('config/compose/'+compose.name+'/docker-compose.yml', "a") + f.write(compose.content) + f.close() + + return get_compose(name=compose.name) \ No newline at end of file diff --git a/backend/api/routers/app_settings.py b/backend/api/routers/app_settings.py index 411b8c29..9befaf17 100644 --- a/backend/api/routers/app_settings.py +++ b/backend/api/routers/app_settings.py @@ -8,7 +8,7 @@ from ..db import crud, schemas from ..db.models import containers from ..db.database import SessionLocal, engine -from ..utils import get_db +from ..utils.auth import get_db from ..auth import get_active_user from ..actions import apps from ..actions import resources diff --git a/backend/api/utils/compose.py b/backend/api/utils/compose.py index 8037774f..324f4f58 100644 --- a/backend/api/utils/compose.py +++ b/backend/api/utils/compose.py @@ -45,4 +45,5 @@ def get_logo_file(path): file.close() break - return logo \ No newline at end of file + return logo + diff --git a/backend/requirements.txt b/backend/requirements.txt index 1c3b4585..47f69b5c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,7 +8,8 @@ chardet==3.0.4 click==7.1.2 databases==0.3.2 dnspython==2.0.0 -docker==4.3.0 +docker==4.3.1 +docker-compose==1.27.3 email-validator==1.1.1 fastapi==0.60.2 fastapi-users==3.0.6 @@ -26,6 +27,7 @@ python-multipart==0.0.5 PyYAML==5.3.1 requests==2.24.0 six==1.15.0 +sh==1.14.1 SQLAlchemy==1.3.19 starlette==0.13.6 typing-extensions==3.7.4.2 From cc097b0ca4dd833816033ff60727ed5aa1b99b56 Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Tue, 27 Oct 2020 16:46:12 -0700 Subject: [PATCH 03/26] added check for COMPOSE_DIR to ensure it ends with a / --- backend/api/actions/compose.py | 16 +++++++--------- backend/api/settings.py | 8 ++++++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/backend/api/actions/compose.py b/backend/api/actions/compose.py index 8eb3716f..c80600d3 100644 --- a/backend/api/actions/compose.py +++ b/backend/api/actions/compose.py @@ -14,16 +14,15 @@ def compose_action(name, action): compose = get_compose(name) if action == "up": try: - cwd = os.getcwd() - os.chdir(os.path.dirname(compose['path'])) - _action = docker_compose("-f", compose['path'], action, '-d') - os.chdir(cwd) + _action = docker_compose("-f", compose['path'], action, '-d', _cwd=os.path.dirname(compose['path'])) except Exception as exc: - os.chdir(cwd) raise HTTPException(400, exc.stderr.decode('UTF-8').rstrip()) else: - _action = docker_compose("-f", compose['path'], action) + try: + _action = docker_compose("-f", compose['path'], action, _cwd=os.path.dirname(compose['path'])) + except Exception as exc: + raise HTTPException(400, exc.stderr.decode('UTF-8').rstrip()) if _action.stdout.decode('UTF-8').rstrip(): output = _action.stdout.decode('UTF-8').rstrip() elif _action.stderr.decode('UTF-8').rstrip(): @@ -74,9 +73,8 @@ def get_compose(name): networks.append(network) for service in loaded_compose.get('services'): services[service] = loaded_compose['services'][service] - compose_object = {'name': project, 'path': file, 'version': loaded_compose['version'], 'services': services, 'volumes': volumes, "networks": networks} - return compose_object - break + compose_object = {'name': project, 'path': file, 'version': loaded_compose['version'], 'services': services, 'volumes': volumes, "networks": networks} + return compose_object else: raise HTTPException(404, 'Project '+name+' not found' ) diff --git a/backend/api/settings.py b/backend/api/settings.py index e2e1dee4..a9509330 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -4,7 +4,10 @@ basedir = os.path.abspath(os.path.dirname(__file__)) - +def compose_dir_check(): + if not os.environ.get("COMPOSE_DIR", "config/compose/").endswith('/'): + os.environ['COMPOSE_DIR'] += '/' + return os.environ['COMPOSE_DIR'] class Settings(BaseSettings): app_name: str = "Yacht API" SECRET_KEY = os.environ.get("SECRET_KEY", secrets.token_hex(16)) @@ -35,4 +38,5 @@ class Settings(BaseSettings): SQLALCHEMY_DATABASE_URI = os.environ.get( "DATABASE_URL", "sqlite:///config/data.sqlite" ) - COMPOSE_DIR = os.environ.get("COMPOSE_DIR", "config/compose/") \ No newline at end of file + COMPOSE_DIR = compose_dir_check() + From b24adeeb07ff63a025fd7927077955eb023ca99e Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Wed, 28 Oct 2020 09:35:24 -0700 Subject: [PATCH 04/26] building frontend out for projects --- backend/api/routers/compose.py | 10 +- .../src/components/compose/ProjectDetails.vue | 280 ++++++++++++++++++ .../src/components/compose/ProjectList.vue | 191 ++++++++++++ frontend/src/components/nav/Sidebar.vue | 11 + frontend/src/router/index.js | 21 ++ frontend/src/store/index.js | 2 + frontend/src/store/modules/projects.js | 156 ++++++++++ frontend/src/views/Project.vue | 15 + 8 files changed, 683 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/compose/ProjectDetails.vue create mode 100644 frontend/src/components/compose/ProjectList.vue create mode 100644 frontend/src/store/modules/projects.js create mode 100644 frontend/src/views/Project.vue diff --git a/backend/api/routers/compose.py b/backend/api/routers/compose.py index cb44bc9d..cae18eb7 100644 --- a/backend/api/routers/compose.py +++ b/backend/api/routers/compose.py @@ -1,14 +1,18 @@ from fastapi import APIRouter, Depends, HTTPException from typing import List from ..auth import get_active_user -from ..actions.compose import get_compose_projects, compose_action +from ..actions.compose import get_compose_projects, compose_action, get_compose router = APIRouter() @router.get("/") -def get_images(): +def get_projects(): return get_compose_projects() +@router.get("/{project_name}") +def get_project(project_name): + return get_compose(project_name) + @router.get("/{project_name}/{action}") -def actions(project_name, action): +def get_compose_action(project_name, action): return compose_action(project_name, action) \ No newline at end of file diff --git a/frontend/src/components/compose/ProjectDetails.vue b/frontend/src/components/compose/ProjectDetails.vue new file mode 100644 index 00000000..0eb8843a --- /dev/null +++ b/frontend/src/components/compose/ProjectDetails.vue @@ -0,0 +1,280 @@ + + + + + diff --git a/frontend/src/components/compose/ProjectList.vue b/frontend/src/components/compose/ProjectList.vue new file mode 100644 index 00000000..2b5c1eed --- /dev/null +++ b/frontend/src/components/compose/ProjectList.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/frontend/src/components/nav/Sidebar.vue b/frontend/src/components/nav/Sidebar.vue index d964150e..6896da32 100644 --- a/frontend/src/components/nav/Sidebar.vue +++ b/frontend/src/components/nav/Sidebar.vue @@ -88,6 +88,17 @@ export default { } ] }, + { + icon: "mdi-book-open", + text: "Projects", + subLinks: [ + { + text: "View Projects", + to: "/projects", + icon: "mdi-view-list" + } + ] + }, { icon: "mdi-cube-outline", text: "Resources", diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index f4601cfb..1a2c6efd 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -20,6 +20,11 @@ import ApplicationsList from "../components/applications/ApplicationsList.vue"; import ApplicationsForm from "../components/applications/ApplicationsForm.vue"; import ApplicationDeployFromTemplate from "../components/applications/ApplicationDeployFromTemplate.vue"; +// Project +import Project from "../views/Project.vue"; +import ProjectList from "../components/compose/ProjectList.vue" +import ProjectDetails from "../components/compose/ProjectDetails.vue" + // Resources import Resources from "../views/Resources.vue"; // Images @@ -129,6 +134,22 @@ const routes = [ } ] }, + { + path: '/projects', + component: Project, + children: [ + { + name: "View Projects", + path: "/", + component: ProjectList + }, + { + name: "Project Details", + path: ":projectName", + component: ProjectDetails + } + ] + }, { path: "/user", component: UserSettings, diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index 39ec59e2..033b8a50 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -7,6 +7,7 @@ import snackbar from "./modules/snackbar.js"; import images from "./modules/images.js"; import volumes from "./modules/volumes.js"; import networks from "./modules/networks.js"; +import projects from "./modules/projects.js"; Vue.use(Vuex); @@ -45,6 +46,7 @@ export default new Vuex.Store({ images, volumes, networks, + projects, auth, snackbar } diff --git a/frontend/src/store/modules/projects.js b/frontend/src/store/modules/projects.js new file mode 100644 index 00000000..411b25c1 --- /dev/null +++ b/frontend/src/store/modules/projects.js @@ -0,0 +1,156 @@ +import axios from "axios"; +import router from "@/router/index"; + +const state = { + projects: [], + isLoading: false +}; + +const mutations = { + setProjects(state, projects) { + state.projects = projects; + }, + setProject(state, project) { + const idx = state.projects.findIndex(x => x.name === project.name); + if (idx < 0) { + state.projects.push(project); + } else { + state.projects.splice(idx, 1, project); + } + }, + addProject(state, project) { + state.projects.push(project); + }, + removeProject(state, project) { + const idx = state.projects.findIndex(x => x.name === project.name); + if (idx < 0) { + return; + } + state.projects.splice(idx, 1); + }, + setLoading(state, loading) { + state.isLoading = loading; + } +}; + +const actions = { + _readProjects({ commit }) { + const url = "/api/compose/"; + commit("setLoading", true); + return new Promise((resolve, reject) => { + axios + .get(url) + .then(response => { + const projects = response.data; + commit("setLoading", false); + commit("setProjects", projects); + resolve(projects); + }) + .finally(() => { + commit("setLoading", false); + }) + .catch(error => { + commit("snackbar/setErr", error, { root: true }); + reject(error); + }); + }); + }, + readProjects({ commit }) { + commit("setLoading", true); + const url = "/api/compose/"; + axios + .get(url) + .then(response => { + const projects = response.data; + commit("setProjects", projects); + }) + .catch(err => { + commit("snackbar/setErr", err, { root: true }); + }) + .finally(() => { + commit("setLoading", false); + }); + }, + readProject({ commit }, name) { + commit("setLoading", true); + const url = `/api/compose/${name}`; + axios + .get(url) + .then(response => { + console.log(response) + const project = response.data; + commit("setProject", project); + }) + .catch(err => { + commit("snackbar/setErr", err, { root: true }); + }) + .finally(() => { + commit("setLoading", false); + }); + }, + writeProject({ commit }, payload) { + commit("setLoading", true); + const url = "/api/compose/"; + axios + .post(url, payload) + .then(response => { + const projects = response.data; + commit("setProjects", projects); + }) + .catch(err => { + commit("snackbar/setErr", err, { root: true }); + }) + .finally(() => { + commit("setLoading", false); + router.push({ name: "Projects" }); + }); + }, + updateProject({ commit }, id) { + commit("setLoading", true); + const url = `/api/compose/${id}/`; + axios + .get(url) + .then(response => { + const project = response.data; + commit("setProject", project); + }) + .catch(err => { + commit("snackbar/setErr", err, { root: true }); + }) + .finally(() => { + commit("setLoading", false); + }); + }, + deleteProject({ commit }, id) { + commit("setLoading", true); + const url = `/api/compose/${id}`; + axios + .delete(url) + .then(response => { + const project = response.data; + commit("removeProject", project); + }) + .catch(err => { + commit("snackbar/setErr", err, { root: true }); + }) + .finally(() => { + commit("setLoading", false); + }); + } +}; + +const getters = { + getProjectByName(state) { + return name => { + return state.projects.find(x => x.name == name); + }; + } +}; + +export default { + namespaced: true, + state, + mutations, + getters, + actions +}; diff --git a/frontend/src/views/Project.vue b/frontend/src/views/Project.vue new file mode 100644 index 00000000..a002613a --- /dev/null +++ b/frontend/src/views/Project.vue @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file From d3a3d37786d965f421303011905b44e403e00ad0 Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Wed, 28 Oct 2020 09:36:18 -0700 Subject: [PATCH 05/26] building frontend out for projects --- backend/api/routers/compose.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/api/routers/compose.py b/backend/api/routers/compose.py index cae18eb7..32acbafb 100644 --- a/backend/api/routers/compose.py +++ b/backend/api/routers/compose.py @@ -5,14 +5,14 @@ router = APIRouter() -@router.get("/") +@router.get("/", dependencies=[Depends(get_active_user)]) def get_projects(): return get_compose_projects() -@router.get("/{project_name}") +@router.get("/{project_name}", dependencies=[Depends(get_active_user)]) def get_project(project_name): return get_compose(project_name) -@router.get("/{project_name}/{action}") +@router.get("/{project_name}/{action}", dependencies=[Depends(get_active_user)]) def get_compose_action(project_name, action): return compose_action(project_name, action) \ No newline at end of file From 53215b8135ad834fcc7ec3964d27a86577498b27 Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Wed, 28 Oct 2020 10:43:43 -0700 Subject: [PATCH 06/26] added more details to project details --- backend/api/actions/compose.py | 7 +- .../src/components/compose/ProjectDetails.vue | 111 +++++++++++------ .../src/components/compose/ProjectList.vue | 117 +++++++++++++----- frontend/src/store/modules/projects.js | 30 ++--- 4 files changed, 173 insertions(+), 92 deletions(-) diff --git a/backend/api/actions/compose.py b/backend/api/actions/compose.py index c80600d3..ffaf3b64 100644 --- a/backend/api/actions/compose.py +++ b/backend/api/actions/compose.py @@ -29,7 +29,12 @@ def compose_action(name, action): output = _action.stderr.decode('UTF-8').rstrip() else: output = 'No Output' - return {'success': True, 'project': compose['name'], 'action': action, 'output': output} + print( + f"""Project {compose['name']} {action} successful.""" + ) + print(f"""Output: """) + print(output) + return get_compose_projects() def get_compose_projects(): files = find_yml_files(settings.COMPOSE_DIR) diff --git a/frontend/src/components/compose/ProjectDetails.vue b/frontend/src/components/compose/ProjectDetails.vue index 0eb8843a..ebf4e0e9 100644 --- a/frontend/src/components/compose/ProjectDetails.vue +++ b/frontend/src/components/compose/ProjectDetails.vue @@ -10,21 +10,6 @@ /> - - - - - mdi-trash-can-outline - Delete Project - - - {{ project.name }} @@ -71,9 +56,9 @@ - - Services - + + Services + - {{ service }} - - ({{ project.services[service].image || "No Image" }}) - - + {{ service }} + + ({{ project.services[service].image || "No Image" }}) + + - + Image @@ -114,6 +99,22 @@ {{ project.services[service].depends_on.join(", ") }} + + + Restart Policy + + + {{ project.services[service].restart }} + + + + + Read Only + + + {{ project.services[service].read_only }} + + Networks @@ -200,7 +201,42 @@ - + + + Labels + + + + + + + + Label + + + Value + + + + + + + {{ value.split("=")[0] }} + + + {{ value.split("=")[1] }} + + + + + + + + Command @@ -229,20 +265,20 @@ - - Networks - - - {{project.networks.join(", ")}} - + + Networks + + + {{ project.networks.join(", ") }} + - - Volumes - - - {{project.volumes.join(", ")}} - + + Volumes + + + {{ project.volumes.join(", ") }} + @@ -267,7 +303,6 @@ export default { methods: { ...mapActions({ readProject: "projects/readProject", - deleteProject: "projects/deleteProject", }), }, created() { diff --git a/frontend/src/components/compose/ProjectList.vue b/frontend/src/components/compose/ProjectList.vue index 2b5c1eed..96cb5e56 100644 --- a/frontend/src/components/compose/ProjectList.vue +++ b/frontend/src/components/compose/ProjectList.vue @@ -37,6 +37,89 @@ - + mdi-eye @@ -104,34 +187,6 @@ - - - - - Delete the project? - - - Are you sure you want to permanently delete the project?
- This action cannot be revoked. -
- - - - Cancel - - - Delete - - -
-
@@ -169,7 +224,9 @@ export default { methods: { ...mapActions({ readProjects: "projects/readProjects", - writeProjects: "projects/writeProject" + writeProjects: "projects/writeProject", + ProjectAction: "projects/ProjectAction" + }), handleRowClick(item) { this.$router.push({ path: `/projects/${item.name}` }); diff --git a/frontend/src/store/modules/projects.js b/frontend/src/store/modules/projects.js index 411b25c1..dcdb9f02 100644 --- a/frontend/src/store/modules/projects.js +++ b/frontend/src/store/modules/projects.js @@ -61,6 +61,7 @@ const actions = { axios .get(url) .then(response => { + console.log(response) const projects = response.data; commit("setProjects", projects); }) @@ -77,7 +78,6 @@ const actions = { axios .get(url) .then(response => { - console.log(response) const project = response.data; commit("setProject", project); }) @@ -105,38 +105,22 @@ const actions = { router.push({ name: "Projects" }); }); }, - updateProject({ commit }, id) { + ProjectAction({ commit }, { Name, Action }) { commit("setLoading", true); - const url = `/api/compose/${id}/`; + const url = `/api/compose/${Name}/${Action}`; axios .get(url) - .then(response => { - const project = response.data; - commit("setProject", project); + .then((response) => { + const projects = response.data; + commit("setProjects", projects); }) - .catch(err => { + .catch((err) => { commit("snackbar/setErr", err, { root: true }); }) .finally(() => { commit("setLoading", false); }); }, - deleteProject({ commit }, id) { - commit("setLoading", true); - const url = `/api/compose/${id}`; - axios - .delete(url) - .then(response => { - const project = response.data; - commit("removeProject", project); - }) - .catch(err => { - commit("snackbar/setErr", err, { root: true }); - }) - .finally(() => { - commit("setLoading", false); - }); - } }; const getters = { From 007c9525b81c93e523dc70e237909e3731802a06 Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Wed, 28 Oct 2020 10:45:31 -0700 Subject: [PATCH 07/26] added container name to project details --- frontend/src/components/compose/ProjectDetails.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/components/compose/ProjectDetails.vue b/frontend/src/components/compose/ProjectDetails.vue index ebf4e0e9..3684a275 100644 --- a/frontend/src/components/compose/ProjectDetails.vue +++ b/frontend/src/components/compose/ProjectDetails.vue @@ -75,6 +75,14 @@ + + + Container Name + + + {{ project.services[service].container_name }} + + Image From 6204df900d3979c5a7dadf74c050f0f2b01df950 Mon Sep 17 00:00:00 2001 From: SelfhostedPro Date: Wed, 28 Oct 2020 10:53:02 -0700 Subject: [PATCH 08/26] fixed sorting --- frontend/src/components/compose/ProjectList.vue | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/compose/ProjectList.vue b/frontend/src/components/compose/ProjectList.vue index 96cb5e56..7dd4d253 100644 --- a/frontend/src/components/compose/ProjectList.vue +++ b/frontend/src/components/compose/ProjectList.vue @@ -35,7 +35,7 @@ No Projects available. -