Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
ac982668c7 | |||
ed24d59a92 | |||
401e446059 | |||
fc7ecdcd77 | |||
09da354b0e | |||
de234133f9 | |||
ca5803bccc | |||
003204afb1 | |||
44a0409798 | |||
34cb6e5dc7 | |||
360aca36c5 | |||
f5ffe07e77 | |||
5628e4f062 |
6 changed files with 122 additions and 14 deletions
|
@ -56,3 +56,37 @@ jobs:
|
||||||
run: pip install twine
|
run: pip install twine
|
||||||
- name: Upload package to registry
|
- name: Upload package to registry
|
||||||
run: python -m twine upload --repository-url ${{ secrets.REPOSITORY_URL }} -u ${{ secrets.TWINE_DEPLOY_USER }} -p ${{ secrets.TWINE_DEPLOY_PASSWORD }} dist/*
|
run: python -m twine upload --repository-url ${{ secrets.REPOSITORY_URL }} -u ${{ secrets.TWINE_DEPLOY_USER }} -p ${{ secrets.TWINE_DEPLOY_PASSWORD }} dist/*
|
||||||
|
|
||||||
|
build-and-push-container:
|
||||||
|
needs: [ "publish-artifacts" ]
|
||||||
|
runs-on: dind
|
||||||
|
steps:
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Log into Docker Package Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: forgejo.neshweb.net
|
||||||
|
username: ${{ secrets.FORGEJO_USERNAME }}
|
||||||
|
password: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
- name: Build and push to Docker Package Registry
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
build-args: |
|
||||||
|
PACKAGE_VERSION=${{ github.ref_name }}
|
||||||
|
push: true
|
||||||
|
tags: forgejo.neshweb.net/firq/dockge-cli:${{ github.ref_name }}
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: [ build-and-push-container, publish-artifacts ]
|
||||||
|
if: success()
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: Release New Version
|
||||||
|
uses: actions/forgejo-release@v1
|
||||||
|
with:
|
||||||
|
direction: upload
|
||||||
|
url: https://forgejo.neshweb.net
|
||||||
|
release-dir: release
|
||||||
|
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
tag: ${{ github.ref_name }}
|
4
Dockerfile
Normal file
4
Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
FROM forgejo.neshweb.net/ci-docker-images/python-neshweb:3.11
|
||||||
|
|
||||||
|
ARG PACKAGE_VERSION=0.1.0
|
||||||
|
RUN pip install dockge-cli==${PACKAGE_VERSION}
|
64
README.md
64
README.md
|
@ -1,3 +1,67 @@
|
||||||
# dockge-cli
|
# dockge-cli
|
||||||
|
|
||||||
A simple CLI application written in Python for communicating with Dockge using websockets
|
A simple CLI application written in Python for communicating with Dockge using websockets
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Dockge (spoken dock-ge or dockage) is a tool to manage docker-compose stacks from a web ui. It is developed by louislam, who also develops UptimeKuma.
|
||||||
|
|
||||||
|
Dockge itself doesn't offer any kind of API or programmatic access, as it is just intended for managing stacks via UI.
|
||||||
|
|
||||||
|
My current deployment solution for firq.dev and fgo-ta.com is based on Dockge, and I was over it always having to reload the stack whenever I pushed an update. Instead, I wanted to have this as a separate CI step, automatically redeploying a givens stack.
|
||||||
|
|
||||||
|
As Dockge is using a websocket-based system under the hood, it was easy to take a look at how communication occurs. In general, communication is achieved by leveraging socket.io for the data. Since Python already offers a solution for socket.io, it is just a matter of emulating the calls the webui sends and receives.
|
||||||
|
|
||||||
|
In the end, this is the current result that works pretty well for my understanding. I am still trying to improve upon some issues (login times out, stability, features), but in general this works as a fine solution for automatic stack updating.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install it from the custom package index using
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install --extra-index-url https://forgejo.neshweb.net/api/packages/Firq/pypi/simple/ dockge-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativly, install it using this repository. When installing for development, make sure to install with the additional dependencies
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip install -e .[lint,typing]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Call the CLI using `dockge-cli` or `dockge`.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
usage: dockge_cli [-h] [--version] {host,login,logout,list,status,restart,start,stop,down,update,exit,help}
|
||||||
|
|
||||||
|
CLI interface for interacting with Dockge
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{host,login,logout,list,status,restart,start,stop,down,update,exit,help}
|
||||||
|
|
||||||
|
options:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--version show program's version number and exit
|
||||||
|
```
|
||||||
|
|
||||||
|
Help for each individual command can be invoked by calling `dockge-cli help <command>`
|
||||||
|
|
||||||
|
## The magic behind this
|
||||||
|
|
||||||
|
Generally, this makes use of the underlying Websockets API that the Dockge frontend uses to communicate with the server. By analyzing the traffic and looking into the codebase, I was able to reverse most of the packets that are being sent. This allows me to then contruct, send and receive my own packets, making the whole thing work.
|
||||||
|
|
||||||
|
There are some things that need to be taken into account for this: For one, dockge uses socket.io for the websocket communication. This meant I had to find the corresponding socket.io version to get the correct version of python-socketio. In addition, I had to find out how the authorization mechanism behind this works.
|
||||||
|
|
||||||
|
After finishing up the first prototype, the workings are as follows:
|
||||||
|
|
||||||
|
1. A websocket session is established using socket.io - this happens automatically
|
||||||
|
2. After the session is ready, the `login` command is sent together with a provided username and password
|
||||||
|
3. Once the CLI is authorized, the selected command is sent
|
||||||
|
4. The CLI waits for any response values and exits once the command has executed successfully
|
||||||
|
|
||||||
|
To provide a smooth experience, both the credentials and the remote host URI are stored on disk. just like the `docker` cli, the credentials are not encrypted, meaning it is advised to either clear the credentials after use OR to use the `--username` and `--password` parameters. This is especially recommended for CI applications.
|
||||||
|
|
||||||
|
## Known issues
|
||||||
|
|
||||||
|
This CLI does not work when Mullvad is used, as Mullvad actively blocks port forwarding (which python-socketio uses)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from getpass import getpass
|
from getpass import getpass
|
||||||
|
import re
|
||||||
|
|
||||||
from ...models import Credentials
|
from ...models import Credentials
|
||||||
from ...service import storage
|
from ...service import storage
|
||||||
from ...service.communicate import DockgeConnection
|
from ...service.connection import DockgeConnection
|
||||||
from ..utils import stack_formatter, status_formatter, generic_formatter, credential_parser_factory
|
from ..utils import stack_formatter, status_formatter, generic_formatter, credential_parser_factory
|
||||||
|
|
||||||
class FunctionBindings():
|
class FunctionBindings():
|
||||||
|
@ -26,10 +27,12 @@ class FunctionBindings():
|
||||||
host command binding
|
host command binding
|
||||||
"""
|
"""
|
||||||
if len(extra_args) > 0:
|
if len(extra_args) > 0:
|
||||||
res = urlparse(extra_args[0])
|
mat = re.search(r"((\w+\.)?\w+\.\w+(\/.+)?)", extra_args[0], re.IGNORECASE)
|
||||||
|
if mat is None:
|
||||||
|
raise ValueError("Given host did not match regex")
|
||||||
|
res = urlparse(f"https://{mat[0]}")
|
||||||
if all([res.scheme, res.netloc]):
|
if all([res.scheme, res.netloc]):
|
||||||
host = extra_args[0].rstrip("/").replace("https://", "").replace("wss://", "")
|
storage.put("host", mat[0])
|
||||||
storage.put("host", host)
|
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Malformed URL {extra_args[0]}")
|
raise ValueError(f"Malformed URL {extra_args[0]}")
|
||||||
print(storage.get("host"))
|
print(storage.get("host"))
|
||||||
|
|
|
@ -63,6 +63,7 @@ class DockgeConnection:
|
||||||
success = True
|
success = True
|
||||||
else:
|
else:
|
||||||
print("Issue with login procedure")
|
print("Issue with login procedure")
|
||||||
|
print(data)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
# Functions
|
# Functions
|
||||||
|
@ -71,7 +72,7 @@ class DockgeConnection:
|
||||||
Connect to the websocket
|
Connect to the websocket
|
||||||
"""
|
"""
|
||||||
# Dockge uses Socket.io for the websockets, so this URI and params are always the same
|
# Dockge uses Socket.io for the websockets, so this URI and params are always the same
|
||||||
self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket'])
|
self._sio.connect(f"https://{self._host}/socket.io/")
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
|
@ -99,10 +100,11 @@ class DockgeConnection:
|
||||||
"password": storage.get("password", encoded=True),
|
"password": storage.get("password", encoded=True),
|
||||||
"token": ""
|
"token": ""
|
||||||
},
|
},
|
||||||
timeout=5
|
timeout=10
|
||||||
)
|
)
|
||||||
retry = False
|
retry = False
|
||||||
except socketio.exceptions.TimeoutError:
|
except socketio.exceptions.TimeoutError:
|
||||||
|
print("Reached timeout for login, retrying ...")
|
||||||
retry = True
|
retry = True
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
@ -125,42 +127,42 @@ class DockgeConnection:
|
||||||
"""
|
"""
|
||||||
Lists status for a stack
|
Lists status for a stack
|
||||||
"""
|
"""
|
||||||
ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=5)
|
ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=10)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def restart(self, name):
|
def restart(self, name):
|
||||||
"""
|
"""
|
||||||
Restarts a given stack
|
Restarts a given stack
|
||||||
"""
|
"""
|
||||||
ret = self._sio.call("agent", ("", "restartStack", name), timeout=10)
|
ret = self._sio.call("agent", ("", "restartStack", name), timeout=30)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def update(self, name):
|
def update(self, name):
|
||||||
"""
|
"""
|
||||||
Updates a given stack
|
Updates a given stack
|
||||||
"""
|
"""
|
||||||
ret = self._sio.call("agent", ("", "updateStack", name), timeout=10)
|
ret = self._sio.call("agent", ("", "updateStack", name), timeout=30)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def stop(self, name):
|
def stop(self, name):
|
||||||
"""
|
"""
|
||||||
Stops a given stack
|
Stops a given stack
|
||||||
"""
|
"""
|
||||||
ret = self._sio.call("agent", ("", "stopStack", name), timeout=10)
|
ret = self._sio.call("agent", ("", "stopStack", name), timeout=30)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def start(self, name):
|
def start(self, name):
|
||||||
"""
|
"""
|
||||||
Starts a given stack
|
Starts a given stack
|
||||||
"""
|
"""
|
||||||
ret = self._sio.call("agent", ("", "startStack", name), timeout=10)
|
ret = self._sio.call("agent", ("", "startStack", name), timeout=30)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def down(self, name):
|
def down(self, name):
|
||||||
"""
|
"""
|
||||||
Stops and downs a given stack
|
Stops and downs a given stack
|
||||||
"""
|
"""
|
||||||
ret = self._sio.call("agent", ("", "downStack", name), timeout=10)
|
ret = self._sio.call("agent", ("", "downStack", name), timeout=30)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
|
@ -1,9 +1,10 @@
|
||||||
[project]
|
[project]
|
||||||
name = "dockge_cli"
|
name = "dockge_cli"
|
||||||
version = "0.1.0-c.3"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyyaml~=6.0.1",
|
"pyyaml~=6.0.1",
|
||||||
"pydantic~=2.8.0",
|
"pydantic~=2.8.0",
|
||||||
|
"requests~=2.32.3",
|
||||||
"python-socketio~=5.11.3",
|
"python-socketio~=5.11.3",
|
||||||
"websocket-client~=1.8.0",
|
"websocket-client~=1.8.0",
|
||||||
"tabulate ~=0.9.0",
|
"tabulate ~=0.9.0",
|
||||||
|
@ -11,7 +12,7 @@ dependencies = [
|
||||||
requires-python = ">= 3.10"
|
requires-python = ">= 3.10"
|
||||||
authors = [{name = "Firq", email = "firelp42@gmail.com"}]
|
authors = [{name = "Firq", email = "firelp42@gmail.com"}]
|
||||||
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
|
maintainers = [{name = "Firq", email = "firelp42@gmail.com"}]
|
||||||
description = "CLi for interacting with dockge"
|
description = "CLI for interacting with dockge"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
|
Loading…
Add table
Reference in a new issue