Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker compose api #203

Merged
merged 27 commits into from
Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d1dc9cd
added basic docker-compose functionality to api (no-auth)
SelfhostedPro Oct 27, 2020
491281d
fixed requirements; cd into directory to run docker compose and then …
SelfhostedPro Oct 27, 2020
cc097b0
added check for COMPOSE_DIR to ensure it ends with a /
SelfhostedPro Oct 27, 2020
b24adee
building frontend out for projects
SelfhostedPro Oct 28, 2020
d3a3d37
building frontend out for projects
SelfhostedPro Oct 28, 2020
53215b8
added more details to project details
SelfhostedPro Oct 28, 2020
007c952
added container name to project details
SelfhostedPro Oct 28, 2020
6204df9
fixed sorting
SelfhostedPro Oct 28, 2020
51949c0
added 'create' (changed to up --no-start on the backend)
SelfhostedPro Oct 28, 2020
34a7e86
update readme with environment variablels
SelfhostedPro Oct 29, 2020
16b8be4
added per app controlls to docker-compose project details
SelfhostedPro Oct 29, 2020
655c0d8
fixed app status not reloading on project app action; added up to com…
SelfhostedPro Oct 29, 2020
118a46f
added --stop to remove running apps when stopped via project details …
SelfhostedPro Oct 29, 2020
0d0d113
added project actions to Project Details view
SelfhostedPro Oct 29, 2020
0982369
blank env variable values are discarded instead of erroring.
SelfhostedPro Oct 29, 2020
0b988d1
added documentation link
SelfhostedPro Oct 29, 2020
7c421db
added documentation link
SelfhostedPro Oct 29, 2020
02c948e
fixed \!PGID variable
SelfhostedPro Oct 29, 2020
c45e4a7
add digitalocean link to the readme
SelfhostedPro Oct 29, 2020
98caa04
add digitalocean link to the readme
SelfhostedPro Oct 29, 2020
cc4b2b4
add digitalocean link to the readme
SelfhostedPro Oct 29, 2020
8d4b6aa
add digitalocean link to the readme
SelfhostedPro Oct 29, 2020
e6bb720
add digitalocean link to the readme
SelfhostedPro Oct 29, 2020
781cec3
add digitalocean link to the readme
SelfhostedPro Oct 29, 2020
6b4dcdc
changed badge colors
SelfhostedPro Oct 29, 2020
931a218
Using unset template variables will now throw an error instead of pas…
SelfhostedPro Oct 29, 2020
f36bdd8
Merge branch 'develop' into docker-compose-api
SelfhostedPro Oct 29, 2020
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[![Docker Image Size](https://img.shields.io/docker/image-size/selfhostedpro/yacht/vue?color=%2341B883&label=Image%20Size&logo=docker&logoColor=%2341B883&style=for-the-badge)](https://hub.docker.com/r/selfhostedpro/yacht)
[![Layers](https://img.shields.io/microbadger/layers/selfhostedpro/yacht?color=%2341B883&label=Layers&logo=docker&logoColor=%2341B883&style=for-the-badge)](https://hub.docker.com/r/selfhostedpro/yacht)
[![Open Collective](https://img.shields.io/opencollective/all/selfhostedpro.svg?color=%2341B883&logoColor=%2341B883&style=for-the-badge&label=Supporters&logo=open%20collective)](https://opencollective.com/selfhostedpro "please consider helping me by either donating or contributing")

## Yacht
Yacht is a container management UI with a focus on templates and 1-click deployments.

Expand All @@ -19,6 +20,10 @@ Installation documentation can be found [here](https://yacht.sh/Installation/yac

Check out the getting started guide if this is the first time you've used Yacht: https://yacht.sh/Installation/gettingstarted/

**Yacht is also available via the DigitalOcean marketplace:**

[![DigitalOcean](https://raw.githubusercontent.com/SelfhostedPro/Yacht/docker-compose-api/readme_media/do-btn-blue.svg)](https://marketplace.digitalocean.com/apps/yacht?refcode=b68dee19dbf6)

## Features So Far:
* Vuetify UI Framework
* Basic Container Management
Expand Down Expand Up @@ -51,5 +56,17 @@ If you're on arm and graphs aren't showing up add the following to your cmdline.
```
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
```
## Supported Environment Variables
You can utilize the following environment variables in Yacht. None of them are manditory.

| Variable | Description |
| ------------- | ------------- |
| PUID | Set userid that the container will run as. |
| PGID | Set groupid that the container will run as. |
| SECRET_KEY | Setting this to a random string ensures you won't be logged out in between reboots of Yacht. |
| ADMIN_EMAIL | This sets the email for the default Yacht user. |
| DISABLE_AUTH | This disables authentication on the backend of Yacht. It's not recommended unless you're using something like Authelia to manage authentication. |
| DATABASE_URL | If you want to have Yacht use a database like SQL instead of the built in sqlite on you can put that info here in the following format: `postgresql://user:password@postgresserver/db` |
| COMPOSE_DIR | This is the path inside the container which contains your folders that have docker compose projects. (*compose tag only*)|
## License
[MIT License](LICENSE.md)
4 changes: 3 additions & 1 deletion backend/api/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
from .apps import *
from .apps import *
from .compose import *
from .resources import *
21 changes: 17 additions & 4 deletions backend/api/actions/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -111,7 +117,10 @@ def deploy_app(template: schemas.DeployForm):
conv_sysctls2data(template.sysctls),
conv_caps2data(template.cap_add),
)

except HTTPException as exc:
raise HTTPException(
status_code=exc.status_code, detail=exc.detail
)
except Exception as exc:
raise HTTPException(
status_code=exc.response.status_code, detail=exc.explanation
Expand Down Expand Up @@ -191,12 +200,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

Expand Down
196 changes: 196 additions & 0 deletions backend/api/actions/compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
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

settings = Settings()


def compose_action(name, action):
files = find_yml_files(settings.COMPOSE_DIR)
compose = get_compose(name)
if action == "up":
try:
_action = docker_compose(
"-f",
compose["path"],
action,
"-d",
_cwd=os.path.dirname(compose["path"]),
)
except Exception as exc:
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
elif action == "create":
try:
_action = docker_compose(
"-f",
compose["path"],
"up",
"--no-start",
_cwd=os.path.dirname(compose["path"]),
)
except Exception as exc:
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
else:
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():
output = _action.stderr.decode("UTF-8").rstrip()
else:
output = "No Output"
print(f"""Project {compose['name']} {action} successful.""")
print(f"""Output: """)
print(output)
return get_compose_projects()


def compose_app_action(
name,
action,
app,
):

files = find_yml_files(settings.COMPOSE_DIR)
compose = get_compose(name)
print('docker-compose -f ' + compose["path"] + ' ' + action + ' ' + app)
if action == "up":
try:
_action = docker_compose(
"-f",
compose["path"],
"up",
"-d",
app,
_cwd=os.path.dirname(compose["path"]),
)
except Exception as exc:
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
elif action == "create":
try:
_action = docker_compose(
"-f",
compose["path"],
"up",
"--no-start",
app,
_cwd=os.path.dirname(compose["path"]),
)
except Exception as exc:
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
elif action == "rm":
try:
_action = docker_compose(
"-f",
compose["path"],
"rm",
"--force",
"--stop",
app,
_cwd=os.path.dirname(compose["path"]),
)
except Exception as exc:
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
else:
try:
_action = docker_compose(
"-f",
compose["path"],
action,
app,
_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():
output = _action.stderr.decode("UTF-8").rstrip()
else:
output = "No Output"
print(f"""Project {compose['name']} App {name} {action} successful.""")
print(f"""Output: """)
print(output)
return get_compose_projects()


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


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
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)
2 changes: 1 addition & 1 deletion backend/api/db/schemas/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class VolumesSchema(BaseModel):

class EnvSchema(BaseModel):
label: str
default: str
default: Optional[str]
name: Optional[str]
description: Optional[str]

Expand Down
7 changes: 6 additions & 1 deletion backend/api/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"])


Expand Down
2 changes: 1 addition & 1 deletion backend/api/routers/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 22 additions & 0 deletions backend/api/routers/compose.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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, compose_app_action, get_compose

router = APIRouter()

@router.get("/", dependencies=[Depends(get_active_user)])
def get_projects():
return get_compose_projects()

@router.get("/{project_name}", dependencies=[Depends(get_active_user)])
def get_project(project_name):
return get_compose(project_name)

@router.get("/{project_name}/{action}", dependencies=[Depends(get_active_user)])
def get_compose_action(project_name, action):
return compose_action(project_name, action)

@router.get("/{project_name}/{action}/{app}", dependencies=[Depends(get_active_user)])
def get_compose_app_action(project_name, action, app):
return compose_app_action(project_name, action, app)
11 changes: 8 additions & 3 deletions backend/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -30,8 +33,10 @@ class Settings(BaseSettings):
{"variable": "!localtime", "replacement": "/etc/localtime"},
{"variable": "!logs", "replacement": "/yacht/AppData/Logs"},
{"variable": "!PUID", "replacement": "1000"},
{"variable": "!GUID", "replacement": "100"},
{"variable": "!PGID", "replacement": "100"},
]
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL", "sqlite:///config/data.sqlite"
)
)
COMPOSE_DIR = compose_dir_check()

5 changes: 5 additions & 0 deletions backend/api/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .apps import *
from .auth import *
from .compose import *
from .resources import *
from .templates import *
Loading