Compare commits
2 commits
d79a01cfb1
...
c00aa232fc
Author | SHA1 | Date | |
---|---|---|---|
c00aa232fc | |||
58aea47921 |
16 changed files with 86 additions and 69 deletions
1
dockge_cli/.gitignore
vendored
1
dockge_cli/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
.temp/*
|
|
|
@ -1,7 +0,0 @@
|
||||||
from ...models import Command
|
|
||||||
from .descriptors import mapping
|
|
||||||
|
|
||||||
commands: dict[str, Command] = {}
|
|
||||||
|
|
||||||
for descriptor in mapping:
|
|
||||||
commands.update({ descriptor.cmd: descriptor })
|
|
4
dockge_cli/client/commands/__init__.py
Normal file
4
dockge_cli/client/commands/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
from ...models import Command
|
||||||
|
from .mappings import mapping
|
||||||
|
|
||||||
|
commands: dict[str, Command] = { c.cmd: c for c in mapping }
|
|
@ -4,15 +4,18 @@ from getpass import getpass
|
||||||
from ...models import Credentials
|
from ...models import Credentials
|
||||||
from ...service import storage
|
from ...service import storage
|
||||||
from ...service.communicate import DockgeConnection
|
from ...service.communicate import DockgeConnection
|
||||||
from ..utils import stack_formatter, status_formatter, generic_formatter, get_credential_parser
|
from ..utils import stack_formatter, status_formatter, generic_formatter, credential_parser_factory
|
||||||
|
|
||||||
class ExecutionCommands():
|
class FunctionBindings():
|
||||||
"""
|
"""
|
||||||
Helper class that provides all the static methods in an organized way
|
Helper class that provides all the static methods in an organized way
|
||||||
This is an abstraction layer of the CLI, as those functions only do little preprocessing before calling the actural DockgeConnection
|
This is an abstraction layer of the CLI, as those functions only do little preprocessing before calling the actural DockgeConnection
|
||||||
"""
|
"""
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __setup():
|
def __setup():
|
||||||
|
"""
|
||||||
|
Creates a connection and logs into Dockge
|
||||||
|
"""
|
||||||
con = DockgeConnection()
|
con = DockgeConnection()
|
||||||
con.connect_and_login()
|
con.connect_and_login()
|
||||||
return con
|
return con
|
||||||
|
@ -36,8 +39,9 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
login command binding
|
login command binding
|
||||||
"""
|
"""
|
||||||
|
print(f"WARNING! These credentials will be saved unencrypted in {storage._file.absolute()}")
|
||||||
if len(extra_args) > 0:
|
if len(extra_args) > 0:
|
||||||
credentials = get_credential_parser().parse_args(extra_args, namespace=Credentials)
|
credentials = credential_parser_factory().parse_args(extra_args, namespace=Credentials)
|
||||||
storage.put("username", credentials.username, encoded=True)
|
storage.put("username", credentials.username, encoded=True)
|
||||||
storage.put("password", credentials.password, encoded=True)
|
storage.put("password", credentials.password, encoded=True)
|
||||||
return
|
return
|
||||||
|
@ -64,7 +68,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
list command binding
|
list command binding
|
||||||
"""
|
"""
|
||||||
con = ExecutionCommands.__setup()
|
con = FunctionBindings.__setup()
|
||||||
stack_formatter(con.list_stacks())
|
stack_formatter(con.list_stacks())
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
||||||
|
@ -73,9 +77,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
status command binding
|
status command binding
|
||||||
"""
|
"""
|
||||||
if extra_args is None:
|
con = FunctionBindings.__setup()
|
||||||
raise ValueError
|
|
||||||
con = ExecutionCommands.__setup()
|
|
||||||
status_formatter(con.list_stack(extra_args[0]))
|
status_formatter(con.list_stack(extra_args[0]))
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
||||||
|
@ -84,9 +86,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
restart command binding
|
restart command binding
|
||||||
"""
|
"""
|
||||||
if extra_args is None:
|
con = FunctionBindings.__setup()
|
||||||
raise ValueError
|
|
||||||
con = ExecutionCommands.__setup()
|
|
||||||
generic_formatter(con.restart(extra_args[0]))
|
generic_formatter(con.restart(extra_args[0]))
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
||||||
|
@ -95,9 +95,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
update command binding
|
update command binding
|
||||||
"""
|
"""
|
||||||
if extra_args is None:
|
con = FunctionBindings.__setup()
|
||||||
raise ValueError
|
|
||||||
con = ExecutionCommands.__setup()
|
|
||||||
generic_formatter(con.update(extra_args[0]))
|
generic_formatter(con.update(extra_args[0]))
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
||||||
|
@ -106,9 +104,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
stop command binding
|
stop command binding
|
||||||
"""
|
"""
|
||||||
if extra_args is None:
|
con = FunctionBindings.__setup()
|
||||||
raise ValueError
|
|
||||||
con = ExecutionCommands.__setup()
|
|
||||||
generic_formatter(con.stop(extra_args[0]))
|
generic_formatter(con.stop(extra_args[0]))
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
||||||
|
@ -117,9 +113,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
start command binding
|
start command binding
|
||||||
"""
|
"""
|
||||||
if extra_args is None:
|
con = FunctionBindings.__setup()
|
||||||
raise ValueError
|
|
||||||
con = ExecutionCommands.__setup()
|
|
||||||
generic_formatter(con.start(extra_args[0]))
|
generic_formatter(con.start(extra_args[0]))
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
||||||
|
@ -128,9 +122,7 @@ class ExecutionCommands():
|
||||||
"""
|
"""
|
||||||
down command binding
|
down command binding
|
||||||
"""
|
"""
|
||||||
if extra_args is None:
|
con = FunctionBindings.__setup()
|
||||||
raise ValueError
|
|
||||||
con = ExecutionCommands.__setup()
|
|
||||||
generic_formatter(con.down(extra_args[0]))
|
generic_formatter(con.down(extra_args[0]))
|
||||||
con.disconnect()
|
con.disconnect()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import List
|
from typing import List
|
||||||
from ...models import Command
|
from ...models import Command
|
||||||
from .bindings import ExecutionCommands
|
from .functions import FunctionBindings
|
||||||
|
|
||||||
mapping: List[Command] = [
|
mapping: List[Command] = [
|
||||||
Command(
|
Command(
|
||||||
|
@ -8,83 +8,83 @@ mapping: List[Command] = [
|
||||||
description="Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI",
|
description="Sets and gets the URI of the dockge instance. Remove any unnecessary subdomains/protocols from the URI",
|
||||||
args=1,
|
args=1,
|
||||||
optional=True,
|
optional=True,
|
||||||
bind=ExecutionCommands.host
|
func=FunctionBindings.host
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="login",
|
cmd="login",
|
||||||
description="Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password",
|
description="Logs into a given dockge account, either with an interactive dialogue or by passing --user and --password",
|
||||||
args=2,
|
args=2,
|
||||||
optional=True,
|
optional=True,
|
||||||
bind=ExecutionCommands.login
|
func=FunctionBindings.login
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="logout",
|
cmd="logout",
|
||||||
description="Removes the credentials from the local storage.",
|
description="Removes the credentials from the local storage.",
|
||||||
args=0,
|
args=0,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.logout
|
func=FunctionBindings.logout
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="list",
|
cmd="list",
|
||||||
description="Lists all available stacks with their status",
|
description="Lists all available stacks with their status",
|
||||||
args=0,
|
args=0,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.list
|
func=FunctionBindings.list
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="status",
|
cmd="status",
|
||||||
description="Returns the status of one stack",
|
description="Returns the status of one stack",
|
||||||
args=1,
|
args=1,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.status
|
func=FunctionBindings.status
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="restart",
|
cmd="restart",
|
||||||
description="Restarts a given stack",
|
description="Restarts a given stack",
|
||||||
args=1,
|
args=1,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.restart
|
func=FunctionBindings.restart
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="start",
|
cmd="start",
|
||||||
description="Starts a given stack",
|
description="Starts a given stack",
|
||||||
args=1,
|
args=1,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.start
|
func=FunctionBindings.start
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="stop",
|
cmd="stop",
|
||||||
description="Stops a given stack",
|
description="Stops a given stack",
|
||||||
args=1,
|
args=1,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.stop
|
func=FunctionBindings.stop
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="down",
|
cmd="down",
|
||||||
description="Stop & Downs a given stack",
|
description="Stop & Downs a given stack",
|
||||||
args=1,
|
args=1,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.down
|
func=FunctionBindings.down
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="update",
|
cmd="update",
|
||||||
description="Updates a stack",
|
description="Updates a stack",
|
||||||
args=1,
|
args=1,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.update
|
func=FunctionBindings.update
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="exit",
|
cmd="exit",
|
||||||
description="Exits the CLI - this will reset all settings, including credentials and host",
|
description="Exits the CLI - this will reset all settings, including credentials and host",
|
||||||
args=0,
|
args=0,
|
||||||
optional=False,
|
optional=False,
|
||||||
bind=ExecutionCommands.exit
|
func=FunctionBindings.exit
|
||||||
),
|
),
|
||||||
Command(
|
Command(
|
||||||
cmd="help",
|
cmd="help",
|
||||||
description="Displays helping hints for commands",
|
description="Displays helping hints for commands",
|
||||||
args=1,
|
args=1,
|
||||||
optional=True,
|
optional=True,
|
||||||
bind=ExecutionCommands.help
|
func=FunctionBindings.help
|
||||||
)
|
)
|
||||||
]
|
]
|
|
@ -3,7 +3,7 @@ import sys
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..models import Arguments
|
from ..models import Arguments
|
||||||
from .commandprovider.factory import commands
|
from .commands import commands
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from .commandprovider.factory import commands
|
from .commands import commands
|
||||||
|
|
||||||
def display_help(extra_args):
|
def display_help(extra_args):
|
||||||
"""
|
"""
|
||||||
|
@ -32,4 +32,4 @@ def run(command, args):
|
||||||
display_help(args)
|
display_help(args)
|
||||||
return
|
return
|
||||||
|
|
||||||
c.bind(args)
|
c.func(args)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import argparse
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
from ..models import StackStatus
|
from ..models import StackStatus
|
||||||
|
|
||||||
def get_credential_parser():
|
def credential_parser_factory():
|
||||||
"""
|
"""
|
||||||
Creates a new parser for login credentials
|
Creates a new parser for login credentials
|
||||||
"""
|
"""
|
||||||
|
@ -23,7 +23,7 @@ def stack_formatter(stacks):
|
||||||
|
|
||||||
table, headers = [], ["Stackname", "Status"]
|
table, headers = [], ["Stackname", "Status"]
|
||||||
for key, val in stacks["stackList"].items():
|
for key, val in stacks["stackList"].items():
|
||||||
table.append([key, StackStatus(val['status']).name])
|
table.append([key, StackStatus(val['status']).name.lower()])
|
||||||
|
|
||||||
print(tabulate(table, headers=headers, tablefmt="github"), "\n")
|
print(tabulate(table, headers=headers, tablefmt="github"), "\n")
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from .codes import StackStatus
|
from .codes import StackStatus
|
||||||
from .commands import Command
|
from .command import Command
|
||||||
from .parser import Arguments, Credentials
|
from .parser import Arguments, Credentials
|
||||||
|
|
|
@ -2,9 +2,8 @@ from enum import Enum
|
||||||
|
|
||||||
class StackStatus(Enum):
|
class StackStatus(Enum):
|
||||||
"""
|
"""
|
||||||
mapping codes for status vs text
|
mapping for plaintext vs statuscode
|
||||||
"""
|
"""
|
||||||
# pylint: disable=invalid-name
|
INACTIVE = 1
|
||||||
inactive = 1
|
RUNNING = 3
|
||||||
running = 3
|
EXITED = 4
|
||||||
exited = 4
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ class Command(BaseModel):
|
||||||
Basic command structure for the CLI to automatically generate valid commands
|
Basic command structure for the CLI to automatically generate valid commands
|
||||||
"""
|
"""
|
||||||
cmd: str
|
cmd: str
|
||||||
bind: Callable
|
func: Callable
|
||||||
args: int
|
args: int
|
||||||
optional: bool
|
optional: bool
|
||||||
description: str
|
description: str
|
1
dockge_cli/service/.gitignore
vendored
Normal file
1
dockge_cli/service/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
.storage/*
|
|
@ -32,7 +32,7 @@ class DockgeConnection:
|
||||||
def _init_events(self):
|
def _init_events(self):
|
||||||
@self._sio.event
|
@self._sio.event
|
||||||
def connect():
|
def connect():
|
||||||
self.connect()
|
self.login()
|
||||||
print("Connected!")
|
print("Connected!")
|
||||||
|
|
||||||
@self._sio.event
|
@self._sio.event
|
||||||
|
@ -70,10 +70,11 @@ 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
|
||||||
self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket'])
|
self._sio.connect(f"https://{self._host}/socket.io/", transports=['websocket'])
|
||||||
self.connect()
|
self.login()
|
||||||
|
|
||||||
def connect(self):
|
def login(self):
|
||||||
"""
|
"""
|
||||||
Log into dockge using basicauth
|
Log into dockge using basicauth
|
||||||
Retries 5 times when timeouts occur
|
Retries 5 times when timeouts occur
|
||||||
|
@ -84,6 +85,11 @@ class DockgeConnection:
|
||||||
data = None
|
data = None
|
||||||
retry, count = True, 0
|
retry, count = True, 0
|
||||||
|
|
||||||
|
if not storage.exists("username"):
|
||||||
|
raise ValueError("Missing username")
|
||||||
|
if not storage.exists("password"):
|
||||||
|
raise ValueError("Missing password")
|
||||||
|
|
||||||
while retry and count < 5:
|
while retry and count < 5:
|
||||||
try:
|
try:
|
||||||
data = self._sio.call(
|
data = self._sio.call(
|
||||||
|
@ -91,7 +97,7 @@ class DockgeConnection:
|
||||||
{
|
{
|
||||||
"username": storage.get("username", encoded=True),
|
"username": storage.get("username", encoded=True),
|
||||||
"password": storage.get("password", encoded=True),
|
"password": storage.get("password", encoded=True),
|
||||||
"token":""
|
"token": ""
|
||||||
},
|
},
|
||||||
timeout=5
|
timeout=5
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,28 +3,44 @@ import pathlib
|
||||||
import base64
|
import base64
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
_storagepath = pathlib.Path(__file__).parents[1] / ".temp"
|
_storagepath = pathlib.Path(__file__).parent / ".storage"
|
||||||
|
_storagepath.mkdir(exist_ok=True, parents=True)
|
||||||
_file = _storagepath / "storage.yaml"
|
_file = _storagepath / "storage.yaml"
|
||||||
|
|
||||||
_storagepath.mkdir(exist_ok=True, parents=True)
|
def create_file_when_missing():
|
||||||
|
|
||||||
def fileexists():
|
|
||||||
"""
|
"""
|
||||||
Checks if storage file does exist, creates it when necessary
|
Checks if storage file does exist, creates it when necessary
|
||||||
"""
|
"""
|
||||||
|
if _file.exists():
|
||||||
|
return
|
||||||
|
with open(_file, 'a', encoding="utf-8"):
|
||||||
|
os.utime(_file, None)
|
||||||
|
|
||||||
|
def exists(key: str) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if a given key exists in the storage file
|
||||||
|
"""
|
||||||
if not _file.exists():
|
if not _file.exists():
|
||||||
with open(_file, 'a', encoding="utf-8"):
|
return False
|
||||||
os.utime(_file, None)
|
|
||||||
|
with open(_file, "r", encoding="utf-8") as file:
|
||||||
|
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
|
||||||
|
|
||||||
|
return key in content
|
||||||
|
|
||||||
|
|
||||||
def put(key: str, value: str, encoded=False):
|
def put(key: str, value: str, encoded=False):
|
||||||
"""
|
"""
|
||||||
Puts a given value with a given key into the storage file
|
Puts a given value with a given key into the storage file
|
||||||
Encodes the data as base64 when encoded is set to true
|
Encodes the data as base64 when encoded is set to true
|
||||||
"""
|
"""
|
||||||
fileexists()
|
if not _file.exists():
|
||||||
with open(_file, "r+", encoding="utf-8") as file:
|
create_file_when_missing()
|
||||||
|
|
||||||
|
with open(_file, "r", encoding="utf-8") as file:
|
||||||
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
|
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
|
||||||
content.update({ key: str(base64.b64encode(value.encode()), "utf-8") if encoded else value })
|
content.update({ key: str(base64.b64encode(value.encode()), "utf-8") if encoded else value })
|
||||||
|
|
||||||
with open(_file, "w+", encoding="utf-8") as file:
|
with open(_file, "w+", encoding="utf-8") as file:
|
||||||
yaml.dump(content, file, Dumper=yaml.SafeDumper)
|
yaml.dump(content, file, Dumper=yaml.SafeDumper)
|
||||||
|
|
||||||
|
@ -32,10 +48,13 @@ def remove(key: str):
|
||||||
"""
|
"""
|
||||||
Removed a given key from the storage file
|
Removed a given key from the storage file
|
||||||
"""
|
"""
|
||||||
fileexists()
|
if not _file.exists():
|
||||||
|
create_file_when_missing()
|
||||||
|
|
||||||
with open(_file, "r", encoding="utf-8") as file:
|
with open(_file, "r", encoding="utf-8") as file:
|
||||||
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
|
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader) or {}
|
||||||
content.pop(key, None)
|
content.pop(key, None)
|
||||||
|
|
||||||
with open(_file, "w+", encoding="utf-8") as file:
|
with open(_file, "w+", encoding="utf-8") as file:
|
||||||
yaml.dump(content, file, Dumper=yaml.SafeDumper)
|
yaml.dump(content, file, Dumper=yaml.SafeDumper)
|
||||||
|
|
||||||
|
@ -47,9 +66,11 @@ def get(key: str, encoded=False):
|
||||||
value: str | None = None
|
value: str | None = None
|
||||||
if not _file.exists():
|
if not _file.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
with open(_file, "r", encoding="utf-8") as file:
|
with open(_file, "r", encoding="utf-8") as file:
|
||||||
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
|
content: dict[str, str] = yaml.load(file, Loader=yaml.SafeLoader)
|
||||||
value = content.get(key, None)
|
value = content.get(key, None)
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return base64.b64decode(value.encode()).decode() if encoded else value
|
return base64.b64decode(value.encode()).decode() if encoded else value
|
||||||
|
@ -58,4 +79,6 @@ def clear():
|
||||||
"""
|
"""
|
||||||
Deletes the storage file
|
Deletes the storage file
|
||||||
"""
|
"""
|
||||||
|
if not _file.exists():
|
||||||
|
return
|
||||||
_file.unlink()
|
_file.unlink()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "dockge_cli"
|
name = "dockge_cli"
|
||||||
version = "0.1.0-c.2"
|
version = "0.1.0-c.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyyaml~=6.0.1",
|
"pyyaml~=6.0.1",
|
||||||
"pydantic~=2.8.0",
|
"pydantic~=2.8.0",
|
||||||
|
|
Loading…
Reference in a new issue