Skip to content

Commit 58ad2bc

Browse files
Merge pull request #204 from SelfhostedPro/develop
Alpha 5 (v0.0.5-alpha)
2 parents a0ab6f7 + b4a8921 commit 58ad2bc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2400
-933
lines changed

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
![logo](https://raw.githubusercontent.com/SelfhostedPro/Yacht/master/readme_media/Yacht_logo_1_dark.png "templates")
22

3-
[![Docker Hub Pulls](https://img.shields.io/docker/pulls/selfhostedpro/yacht?color=%2341B883&label=Docker%20Pulls&logo=docker&logoColor=%23403d3d&style=for-the-badge)](https://hub.docker.com/r/selfhostedpro/yacht)
4-
[![Docker Image Size](https://img.shields.io/docker/image-size/selfhostedpro/yacht/vue?color=%2341B883&label=Image%20Size&logo=docker&logoColor=%23403d3d&style=for-the-badge)](https://hub.docker.com/r/selfhostedpro/yacht)
5-
[![Layers](https://img.shields.io/microbadger/layers/selfhostedpro/yacht?color=%2341B883&label=Layers&logo=docker&logoColor=%23403d3d&style=for-the-badge)](https://hub.docker.com/r/selfhostedpro/yacht)
3+
[![Docker Hub Pulls](https://img.shields.io/docker/pulls/selfhostedpro/yacht?color=%2341B883&label=Docker%20Pulls&logo=docker&logoColor=%2341B883&style=for-the-badge)](https://hub.docker.com/r/selfhostedpro/yacht)
4+
[![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)
5+
[![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)
6+
[![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")
67

78
## Yacht
89
Yacht is a container management UI with a focus on templates and 1-click deployments.
@@ -19,6 +20,10 @@ Installation documentation can be found [here](https://yacht.sh/Installation/yac
1920

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

23+
**Yacht is also available via the DigitalOcean marketplace:**
24+
25+
[![DigitalOcean](https://raw.githubusercontent.com/SelfhostedPro/Yacht/develop/readme_media/do-btn-blue.svg)](https://marketplace.digitalocean.com/apps/yacht?refcode=b68dee19dbf6)
26+
2227
## Features So Far:
2328
* Vuetify UI Framework
2429
* Basic Container Management
@@ -51,5 +56,17 @@ If you're on arm and graphs aren't showing up add the following to your cmdline.
5156
```
5257
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
5358
```
59+
## Supported Environment Variables
60+
You can utilize the following environment variables in Yacht. None of them are manditory.
61+
62+
| Variable | Description |
63+
| ------------- | ------------- |
64+
| PUID | Set userid that the container will run as. |
65+
| PGID | Set groupid that the container will run as. |
66+
| SECRET_KEY | Setting this to a random string ensures you won't be logged out in between reboots of Yacht. |
67+
| ADMIN_EMAIL | This sets the email for the default Yacht user. |
68+
| DISABLE_AUTH | This disables authentication on the backend of Yacht. It's not recommended unless you're using something like Authelia to manage authentication. |
69+
| 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` |
70+
| COMPOSE_DIR | This is the path inside the container which contains your folders that have docker compose projects. (*compose tag only*)|
5471
## License
5572
[MIT License](LICENSE.md)

backend/api/actions/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
from .apps import *
1+
from .apps import *
2+
from .compose import *
3+
from .resources import *

backend/api/actions/apps.py

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from ..db import models, schemas
55
from ..utils import *
66
from ..utils import check_updates as _update_check
7+
from docker.errors import APIError
78

89
from datetime import datetime
910
import time
@@ -24,6 +25,7 @@ def get_running_apps():
2425

2526
return apps_list
2627

28+
2729
def check_app_update(app_name):
2830
dclient = docker.from_env()
2931
try:
@@ -32,7 +34,7 @@ def check_app_update(app_name):
3234
raise HTTPException(
3335
status_code=exc.response.status_code, detail=exc.explanation
3436
)
35-
37+
3638
if app.attrs["Config"]["Image"]:
3739
if _update_check(app.attrs["Config"]["Image"]):
3840
app.attrs.update(conv2dict("isUpdatable", True))
@@ -41,10 +43,16 @@ def check_app_update(app_name):
4143
app.attrs.update(conv2dict("short_id", app.short_id))
4244
return app.attrs
4345

46+
4447
def get_apps():
4548
apps_list = []
4649
dclient = docker.from_env()
47-
apps = dclient.containers.list(all=True)
50+
try:
51+
apps = dclient.containers.list(all=True)
52+
except Exception as exc:
53+
raise HTTPException(
54+
status_code=exc.response.status_code, detail=exc.explanation
55+
)
4856
for app in apps:
4957
attrs = app.attrs
5058

@@ -111,7 +119,8 @@ def deploy_app(template: schemas.DeployForm):
111119
conv_sysctls2data(template.sysctls),
112120
conv_caps2data(template.cap_add),
113121
)
114-
122+
except HTTPException as exc:
123+
raise HTTPException(status_code=exc.status_code, detail=exc.detail)
115124
except Exception as exc:
116125
raise HTTPException(
117126
status_code=exc.response.status_code, detail=exc.explanation
@@ -191,12 +200,16 @@ def app_action(app_name, action):
191200
try:
192201
_action(force=True)
193202
except Exception as exc:
194-
err = f"{exc}"
203+
raise HTTPException(
204+
status_code=exc.response.status_code, detail=exc.explanation
205+
)
195206
else:
196207
try:
197208
_action()
198209
except Exception as exc:
199-
err = exc.explination
210+
raise HTTPException(
211+
status_code=exc.response.status_code, detail=exc.explanation
212+
)
200213
apps_list = get_apps()
201214
return apps_list
202215

@@ -221,7 +234,7 @@ def app_update(app_name):
221234
try:
222235
updater = dclient.containers.run(
223236
image="containrrr/watchtower:latest",
224-
command="--run-once " + old.name,
237+
command="--cleanup --run-once " + old.name,
225238
remove=True,
226239
detach=True,
227240
volumes=volumes,
@@ -263,12 +276,12 @@ def update_self():
263276
print("**** Updating " + yacht.name + "****")
264277
updater = dclient.containers.run(
265278
image="containrrr/watchtower:latest",
266-
command="--run-once " + yacht.name,
279+
command="--cleanup --run-once " + yacht.name,
267280
remove=True,
268281
detach=True,
269282
volumes=volumes,
270283
)
271-
result = updater.wait(timeout=120)
284+
result = updater
272285
print(result)
273286
time.sleep(1)
274287
return result
@@ -284,14 +297,18 @@ def check_self_update():
284297
yacht = dclient.containers.get(yacht_id)
285298
except Exception as exc:
286299
print(exc)
287-
if exc.response.status_code == 404:
300+
if hasattr(exc, 'response') and exc.response.status_code == 404:
288301
raise HTTPException(
289302
status_code=exc.response.status_code,
290303
detail="Unable to get Yacht container ID",
291304
)
292-
else:
305+
elif hasattr(exc, 'response'):
293306
raise HTTPException(
294307
status_code=exc.response.status_code, detail=exc.explanation
295308
)
309+
else:
310+
raise HTTPException(
311+
status_code=400, detail=exc.args
312+
)
296313

297-
return check_updates(yacht.image.tags[0])
314+
return _update_check(yacht.image.tags[0])

backend/api/actions/compose.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
from fastapi import HTTPException
2+
from sh import docker_compose
3+
import os
4+
import yaml
5+
import pathlib
6+
7+
from ..settings import Settings
8+
from ..utils.compose import find_yml_files, get_readme_file, get_logo_file
9+
10+
settings = Settings()
11+
12+
13+
def compose_action(name, action):
14+
files = find_yml_files(settings.COMPOSE_DIR)
15+
compose = get_compose(name)
16+
if action == "up":
17+
try:
18+
_action = docker_compose(
19+
"-f",
20+
compose["path"],
21+
action,
22+
"-d",
23+
_cwd=os.path.dirname(compose["path"]),
24+
)
25+
except Exception as exc:
26+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
27+
elif action == "create":
28+
try:
29+
_action = docker_compose(
30+
"-f",
31+
compose["path"],
32+
"up",
33+
"--no-start",
34+
_cwd=os.path.dirname(compose["path"]),
35+
)
36+
except Exception as exc:
37+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
38+
else:
39+
try:
40+
_action = docker_compose(
41+
"-f", compose["path"], action, _cwd=os.path.dirname(compose["path"])
42+
)
43+
except Exception as exc:
44+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
45+
if _action.stdout.decode("UTF-8").rstrip():
46+
output = _action.stdout.decode("UTF-8").rstrip()
47+
elif _action.stderr.decode("UTF-8").rstrip():
48+
output = _action.stderr.decode("UTF-8").rstrip()
49+
else:
50+
output = "No Output"
51+
print(f"""Project {compose['name']} {action} successful.""")
52+
print(f"""Output: """)
53+
print(output)
54+
return get_compose_projects()
55+
56+
57+
def compose_app_action(
58+
name,
59+
action,
60+
app,
61+
):
62+
63+
files = find_yml_files(settings.COMPOSE_DIR)
64+
compose = get_compose(name)
65+
print("docker-compose -f " + compose["path"] + " " + action + " " + app)
66+
if action == "up":
67+
try:
68+
_action = docker_compose(
69+
"-f",
70+
compose["path"],
71+
"up",
72+
"-d",
73+
app,
74+
_cwd=os.path.dirname(compose["path"]),
75+
)
76+
except Exception as exc:
77+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
78+
elif action == "create":
79+
try:
80+
_action = docker_compose(
81+
"-f",
82+
compose["path"],
83+
"up",
84+
"--no-start",
85+
app,
86+
_cwd=os.path.dirname(compose["path"]),
87+
)
88+
except Exception as exc:
89+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
90+
elif action == "rm":
91+
try:
92+
_action = docker_compose(
93+
"-f",
94+
compose["path"],
95+
"rm",
96+
"--force",
97+
"--stop",
98+
app,
99+
_cwd=os.path.dirname(compose["path"]),
100+
)
101+
except Exception as exc:
102+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
103+
else:
104+
try:
105+
_action = docker_compose(
106+
"-f",
107+
compose["path"],
108+
action,
109+
app,
110+
_cwd=os.path.dirname(compose["path"]),
111+
)
112+
except Exception as exc:
113+
raise HTTPException(400, exc.stderr.decode("UTF-8").rstrip())
114+
if _action.stdout.decode("UTF-8").rstrip():
115+
output = _action.stdout.decode("UTF-8").rstrip()
116+
elif _action.stderr.decode("UTF-8").rstrip():
117+
output = _action.stderr.decode("UTF-8").rstrip()
118+
else:
119+
output = "No Output"
120+
print(f"""Project {compose['name']} App {name} {action} successful.""")
121+
print(f"""Output: """)
122+
print(output)
123+
return get_compose_projects()
124+
125+
126+
def get_compose_projects():
127+
files = find_yml_files(settings.COMPOSE_DIR)
128+
129+
projects = []
130+
for project, file in files.items():
131+
volumes = []
132+
networks = []
133+
services = {}
134+
compose = open(file)
135+
loaded_compose = yaml.load(compose, Loader=yaml.SafeLoader)
136+
if loaded_compose:
137+
if loaded_compose.get("volumes"):
138+
for volume in loaded_compose.get("volumes"):
139+
volumes.append(volume)
140+
if loaded_compose.get("networks"):
141+
for network in loaded_compose.get("networks"):
142+
networks.append(network)
143+
for service in loaded_compose.get("services"):
144+
services[service] = loaded_compose["services"][service]
145+
_project = {
146+
"name": project,
147+
"path": file,
148+
"version": loaded_compose["version"],
149+
"services": services,
150+
"volumes": volumes,
151+
"networks": networks,
152+
}
153+
projects.append(_project)
154+
else:
155+
print("ERROR: " + file + " is invalid or empty!")
156+
return projects
157+
158+
159+
def get_compose(name):
160+
try:
161+
files = find_yml_files(settings.COMPOSE_DIR + name)
162+
except Exception as exc:
163+
print(exc)
164+
for project, file in files.items():
165+
if name == project:
166+
networks = []
167+
volumes = []
168+
services = {}
169+
compose = open(file)
170+
loaded_compose = yaml.load(compose, Loader=yaml.SafeLoader)
171+
if loaded_compose.get("volumes"):
172+
for volume in loaded_compose.get("volumes"):
173+
volumes.append(volume)
174+
if loaded_compose.get("networks"):
175+
for network in loaded_compose.get("networks"):
176+
networks.append(network)
177+
for service in loaded_compose.get("services"):
178+
services[service] = loaded_compose["services"][service]
179+
compose_object = {
180+
"name": project,
181+
"path": file,
182+
"version": loaded_compose["version"],
183+
"services": services,
184+
"volumes": volumes,
185+
"networks": networks,
186+
}
187+
return compose_object
188+
else:
189+
raise HTTPException(404, "Project " + name + " not found")
190+
191+
192+
def write_compose(compose):
193+
print(compose)
194+
pathlib.Path("config/compose/" + compose.name).mkdir(parents=True)
195+
f = open("config/compose/" + compose.name + "/docker-compose.yml", "a")
196+
f.write(compose.content)
197+
f.close()
198+
199+
return get_compose(name=compose.name)

backend/api/actions/resources.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,4 @@ def prune_resources(resource):
281281
deleted_resource = action.prune(filters={"dangling": False})
282282
else:
283283
deleted_resource = action.prune()
284-
return deleted_resource
284+
return deleted_resource

backend/api/auth/auth.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,7 @@ class UserTable(Base, SQLAlchemyBaseUserTable):
6363
app = FastAPI()
6464

6565
fastapi_users = FastAPIUsers(
66-
user_db,
67-
auth_backends,
68-
User,
69-
UserCreate,
70-
UserUpdate,
71-
UserDB,
66+
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB,
7267
)
7368

7469

0 commit comments

Comments
 (0)