diff --git a/.forgejo/workflows/build-release.yaml b/.forgejo/workflows/build-release.yaml index 48e5e92..600b9b0 100644 --- a/.forgejo/workflows/build-release.yaml +++ b/.forgejo/workflows/build-release.yaml @@ -56,37 +56,3 @@ jobs: run: pip install twine - 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/* - - 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 }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 88e754d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM forgejo.neshweb.net/ci-docker-images/python-neshweb:3.11 - -ARG PACKAGE_VERSION=0.1.0 -RUN pip install dockge-cli==${PACKAGE_VERSION} diff --git a/README.md b/README.md index 2f946e3..4c51468 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,3 @@ # dockge-cli 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) diff --git a/dockge_cli/client/commands/functions.py b/dockge_cli/client/commands/functions.py index 3ee695d..f62ed9c 100644 --- a/dockge_cli/client/commands/functions.py +++ b/dockge_cli/client/commands/functions.py @@ -1,10 +1,9 @@ from urllib.parse import urlparse from getpass import getpass -import re from ...models import Credentials from ...service import storage -from ...service.connection import DockgeConnection +from ...service.communicate import DockgeConnection from ..utils import stack_formatter, status_formatter, generic_formatter, credential_parser_factory class FunctionBindings(): @@ -27,12 +26,10 @@ class FunctionBindings(): host command binding """ if len(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]}") + res = urlparse(extra_args[0]) if all([res.scheme, res.netloc]): - storage.put("host", mat[0]) + host = extra_args[0].rstrip("/").replace("https://", "").replace("wss://", "") + storage.put("host", host) else: raise ValueError(f"Malformed URL {extra_args[0]}") print(storage.get("host")) diff --git a/dockge_cli/service/connection.py b/dockge_cli/service/communicate.py similarity index 94% rename from dockge_cli/service/connection.py rename to dockge_cli/service/communicate.py index 78e7582..9fb063b 100644 --- a/dockge_cli/service/connection.py +++ b/dockge_cli/service/communicate.py @@ -63,7 +63,6 @@ class DockgeConnection: success = True else: print("Issue with login procedure") - print(data) return success # Functions @@ -72,7 +71,7 @@ class DockgeConnection: Connect to the websocket """ # 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/") + self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket']) self.login() def login(self): @@ -100,11 +99,10 @@ class DockgeConnection: "password": storage.get("password", encoded=True), "token": "" }, - timeout=10 + timeout=5 ) retry = False except socketio.exceptions.TimeoutError: - print("Reached timeout for login, retrying ...") retry = True count += 1 @@ -127,42 +125,42 @@ class DockgeConnection: """ Lists status for a stack """ - ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=10) + ret = self._sio.call("agent", ("", "serviceStatusList", name), timeout=5) return ret def restart(self, name): """ Restarts a given stack """ - ret = self._sio.call("agent", ("", "restartStack", name), timeout=30) + ret = self._sio.call("agent", ("", "restartStack", name), timeout=10) return ret def update(self, name): """ Updates a given stack """ - ret = self._sio.call("agent", ("", "updateStack", name), timeout=30) + ret = self._sio.call("agent", ("", "updateStack", name), timeout=10) return ret def stop(self, name): """ Stops a given stack """ - ret = self._sio.call("agent", ("", "stopStack", name), timeout=30) + ret = self._sio.call("agent", ("", "stopStack", name), timeout=10) return ret def start(self, name): """ Starts a given stack """ - ret = self._sio.call("agent", ("", "startStack", name), timeout=30) + ret = self._sio.call("agent", ("", "startStack", name), timeout=10) return ret def down(self, name): """ Stops and downs a given stack """ - ret = self._sio.call("agent", ("", "downStack", name), timeout=30) + ret = self._sio.call("agent", ("", "downStack", name), timeout=10) return ret def disconnect(self): diff --git a/pyproject.toml b/pyproject.toml index b706b37..d1abd6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,9 @@ [project] name = "dockge_cli" -version = "0.1.2" +version = "0.1.0-c.3" dependencies = [ "pyyaml~=6.0.1", "pydantic~=2.8.0", - "requests~=2.32.3", "python-socketio~=5.11.3", "websocket-client~=1.8.0", "tabulate ~=0.9.0", @@ -12,7 +11,7 @@ dependencies = [ requires-python = ">= 3.10" authors = [{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 = [ "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3",