Further work - add auth, split CLI, simplify locking
This commit is contained in:
parent
7004b0767e
commit
0b585dee0d
@ -1,3 +0,0 @@
|
|||||||
[spoc]
|
|
||||||
data-dir = /var/lib/spoc/
|
|
||||||
repo-url = https://repo.spotter.cz/spoc/
|
|
@ -22,6 +22,7 @@ classifiers =
|
|||||||
[options]
|
[options]
|
||||||
packages = find:
|
packages = find:
|
||||||
package_dir = = src
|
package_dir = = src
|
||||||
|
py_modules = spoc_cli
|
||||||
python_requires = >= 3.5
|
python_requires = >= 3.5
|
||||||
install_requires = requests
|
install_requires = requests
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ where = src
|
|||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
spoc = spoc:main
|
spoc = spoc_cli:main
|
||||||
|
|
||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
testpaths = tests
|
testpaths = tests
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import argparse
|
|
||||||
import os
|
|
||||||
from pkg_resources import parse_version
|
from pkg_resources import parse_version
|
||||||
|
|
||||||
from . import app
|
from . import app
|
||||||
@ -9,144 +7,98 @@ from . import podman
|
|||||||
from . import repo
|
from . import repo
|
||||||
from .flock import locked
|
from .flock import locked
|
||||||
|
|
||||||
def print_lock(pid):
|
|
||||||
with open(os.path.join('/proc', pid, 'cmdline')) as f:
|
|
||||||
cmdline = f.read().replace('\0', ' ').strip()
|
|
||||||
print(f'Waiting for lock currently held by process {pid} - {cmdline}')
|
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
class AppError(Exception):
|
||||||
def listing(list_type):
|
def __init__(self, app_name):
|
||||||
if list_type == 'installed':
|
super().__init__(app_name)
|
||||||
apps = podman.get_apps()
|
self.app_name = app_name
|
||||||
elif list_type == 'online':
|
|
||||||
apps = {app:definition['version'] for app,definition in repo.get_apps().items()}
|
|
||||||
elif list_type == 'updates':
|
|
||||||
online_apps = {app:definition['version'] for app,definition in repo.get_apps().items()}
|
|
||||||
apps = {app:f'{version} -> {online_apps[app]}' for app,version in podman.get_apps().items()
|
|
||||||
if app in online_apps
|
|
||||||
and parse_version(online_apps[app]) > parse_version(version)}
|
|
||||||
else:
|
|
||||||
apps = {}
|
|
||||||
for app_name, app_version in sorted(apps.items()):
|
|
||||||
print(app_name, app_version)
|
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
class AppAlreadyInstalledError(AppError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AppNotInstalledError(AppError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AppNotInRepoError(AppError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AppNotUpdateableError(AppError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def list_installed():
|
||||||
|
return dict(sorted(podman.get_apps().items()))
|
||||||
|
|
||||||
|
def list_online():
|
||||||
|
return {app:definition['version'] for app,definition in sorted(repo.get_apps().items())}
|
||||||
|
|
||||||
|
def list_updates():
|
||||||
|
online_apps = {app:definition['version'] for app,definition in repo.get_apps().items()}
|
||||||
|
apps = {app:f'{version} -> {online_apps[app]}' for app,version in podman.get_apps().items()
|
||||||
|
if app in online_apps
|
||||||
|
and parse_version(online_apps[app]) > parse_version(version)}
|
||||||
|
return dict(sorted(apps.items()))
|
||||||
|
|
||||||
|
@locked()
|
||||||
def install(app_name):
|
def install(app_name):
|
||||||
|
if app_name in podman.get_apps():
|
||||||
|
raise AppAlreadyInstalledError(app_name)
|
||||||
|
if app_name not in repo.get_apps():
|
||||||
|
raise AppNotInRepoError(app_name)
|
||||||
app.install(app_name)
|
app.install(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def update(app_name):
|
def update(app_name):
|
||||||
|
if app_name not in podman.get_apps():
|
||||||
|
raise AppNotInstalledError(app_name)
|
||||||
|
if app_name not in list_updates():
|
||||||
|
raise AppNotUpdateableError(app_name)
|
||||||
app.update(app_name)
|
app.update(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def uninstall(app_name):
|
def uninstall(app_name):
|
||||||
|
if app_name not in podman.get_apps():
|
||||||
|
raise AppNotInstalledError(app_name)
|
||||||
app.uninstall(app_name)
|
app.uninstall(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def start(app_name):
|
def start(app_name):
|
||||||
|
if app_name not in podman.get_apps():
|
||||||
|
raise AppNotInstalledError(app_name)
|
||||||
podman.start_pod(app_name)
|
podman.start_pod(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def stop(app_name):
|
def stop(app_name):
|
||||||
|
if app_name not in podman.get_apps():
|
||||||
|
raise AppNotInstalledError(app_name)
|
||||||
podman.stop_pod(app_name)
|
podman.stop_pod(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
|
||||||
def status(app_name):
|
def status(app_name):
|
||||||
app_status = podman.get_pod_status(app_name)
|
if app_name not in podman.get_apps():
|
||||||
print(app_status)
|
raise AppNotInstalledError(app_name)
|
||||||
|
return podman.get_pod_status(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def set_autostart(app_name, value):
|
def set_autostart(app_name, enabled):
|
||||||
enabled = value.lower() in ('1', 'on', 'enable', 'true')
|
if app_name not in podman.get_apps():
|
||||||
|
raise AppNotInstalledError(app_name)
|
||||||
autostart.set_app(app_name, enabled)
|
autostart.set_app(app_name, enabled)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def start_autostarted():
|
def start_autostarted():
|
||||||
for app_name in autostart.get_apps():
|
for app_name in autostart.get_apps():
|
||||||
podman.start_pod(app_name)
|
podman.start_pod(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
def stop_all():
|
def stop_all():
|
||||||
for app_name in podman.get_apps():
|
for app_name in podman.get_apps():
|
||||||
podman.stop_pod(app_name)
|
podman.stop_pod(app_name)
|
||||||
|
|
||||||
@locked(config.LOCK_FILE, print_lock)
|
@locked()
|
||||||
|
def login(host, username, password):
|
||||||
|
config.write_auth(host, username, password)
|
||||||
|
repo.load(force=True)
|
||||||
|
|
||||||
|
@locked()
|
||||||
def prune():
|
def prune():
|
||||||
podman.prune()
|
podman.prune()
|
||||||
|
|
||||||
def parse_args(args=None):
|
|
||||||
parser = argparse.ArgumentParser(description='SPOC application manager')
|
|
||||||
parser.set_defaults(action=None)
|
|
||||||
subparsers = parser.add_subparsers()
|
|
||||||
|
|
||||||
parser_list = subparsers.add_parser('list')
|
|
||||||
parser_list.set_defaults(action=listing)
|
|
||||||
parser_list.add_argument('type', choices=('installed', 'online', 'updates'),
|
|
||||||
default='installed', const='installed', nargs='?',
|
|
||||||
help='Selected repository or application criteria')
|
|
||||||
|
|
||||||
parser_install = subparsers.add_parser('install')
|
|
||||||
parser_install.set_defaults(action=install)
|
|
||||||
parser_install.add_argument('app', help='Name of the application to install')
|
|
||||||
|
|
||||||
parser_update = subparsers.add_parser('update')
|
|
||||||
parser_update.set_defaults(action=update)
|
|
||||||
parser_update.add_argument('app', help='Name of the application to update')
|
|
||||||
|
|
||||||
parser_uninstall = subparsers.add_parser('uninstall')
|
|
||||||
parser_uninstall.set_defaults(action=uninstall)
|
|
||||||
parser_uninstall.add_argument('app', help='Name of the application to uninstall')
|
|
||||||
|
|
||||||
parser_start = subparsers.add_parser('start')
|
|
||||||
parser_start.set_defaults(action=start)
|
|
||||||
parser_start.add_argument('app', help='Name of the application to start')
|
|
||||||
|
|
||||||
parser_stop = subparsers.add_parser('stop')
|
|
||||||
parser_stop.set_defaults(action=stop)
|
|
||||||
parser_stop.add_argument('app', help='Name of the application to stop')
|
|
||||||
|
|
||||||
parser_status = subparsers.add_parser('status')
|
|
||||||
parser_status.set_defaults(action=status)
|
|
||||||
parser_status.add_argument('app', nargs='?', help='Name of the application to check')
|
|
||||||
|
|
||||||
parser_autostart = subparsers.add_parser('autostart')
|
|
||||||
parser_autostart.set_defaults(action=set_autostart)
|
|
||||||
parser_autostart.add_argument('app', help='Name of the application to be automatically started')
|
|
||||||
parser_autostart.add_argument('value', choices=('1', 'on', 'enable', 'true', '0', 'off', 'disable', 'false'), help='Set or unset the applications to be automatically started after the host boots up')
|
|
||||||
|
|
||||||
parser_start_autostarted = subparsers.add_parser('start-autostarted')
|
|
||||||
parser_start_autostarted.set_defaults(action=start_autostarted)
|
|
||||||
|
|
||||||
parser_stop_all = subparsers.add_parser('stop-all')
|
|
||||||
parser_stop_all.set_defaults(action=stop_all)
|
|
||||||
|
|
||||||
parser_prune = subparsers.add_parser('prune')
|
|
||||||
parser_prune.set_defaults(action=prune)
|
|
||||||
|
|
||||||
return parser.parse_args(args)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = parse_args()
|
|
||||||
if args.action is listing:
|
|
||||||
listing(args.type)
|
|
||||||
elif args.action is install:
|
|
||||||
install(args.app)
|
|
||||||
elif args.action is update:
|
|
||||||
update(args.app)
|
|
||||||
elif args.action is uninstall:
|
|
||||||
uninstall(args.app)
|
|
||||||
elif args.action is start:
|
|
||||||
start(args.app)
|
|
||||||
elif args.action is stop:
|
|
||||||
stop(args.app)
|
|
||||||
elif args.action is status:
|
|
||||||
status(args.app)
|
|
||||||
elif args.action is set_autostart:
|
|
||||||
set_autostart(args.app, args.value)
|
|
||||||
elif args.action is start_autostarted:
|
|
||||||
start_autostarted()
|
|
||||||
elif args.action is stop_all:
|
|
||||||
stop_all()
|
|
||||||
elif args.action is prune:
|
|
||||||
prune()
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
from . import autostart
|
||||||
from . import config
|
from . import config
|
||||||
from . import depsolver
|
from . import depsolver
|
||||||
from . import podman
|
from . import podman
|
||||||
@ -44,6 +45,7 @@ class App:
|
|||||||
self.create_containers(containers)
|
self.create_containers(containers)
|
||||||
|
|
||||||
def uninstall(self):
|
def uninstall(self):
|
||||||
|
autostart.set_app(self.app_name, False)
|
||||||
self.remove_pod()
|
self.remove_pod()
|
||||||
self.remove_env_vars()
|
self.remove_env_vars()
|
||||||
self.remove_volumes(self.get_existing_volumes())
|
self.remove_volumes(self.get_existing_volumes())
|
||||||
@ -56,21 +58,21 @@ class App:
|
|||||||
podman.remove_pod(self.app_name)
|
podman.remove_pod(self.app_name)
|
||||||
|
|
||||||
def read_env_vars(self):
|
def read_env_vars(self):
|
||||||
vars = {}
|
env_vars = {}
|
||||||
try:
|
try:
|
||||||
with open(self.env_file) as f:
|
with open(self.env_file) as f:
|
||||||
lines = f.read().splitlines()
|
lines = f.read().splitlines()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
key,value = line.split('=', 1)
|
key,value = line.split('=', 1)
|
||||||
vars[key] = value
|
env_vars[key] = value
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pass
|
pass
|
||||||
return vars
|
return env_vars
|
||||||
|
|
||||||
def write_env_vars(self, vars):
|
def write_env_vars(self, env_vars):
|
||||||
os.makedirs(config.DATA_DIR, exist_ok=True)
|
os.makedirs(config.DATA_DIR, exist_ok=True)
|
||||||
with open(self.env_file, 'w') as f:
|
with open(self.env_file, 'w') as f:
|
||||||
for key,value in vars.items():
|
for key,value in env_vars.items():
|
||||||
f.write(f'{key}={value}\n')
|
f.write(f'{key}={value}\n')
|
||||||
|
|
||||||
def remove_env_vars(self):
|
def remove_env_vars(self):
|
||||||
|
@ -18,7 +18,7 @@ def set_app(app_name, enabled):
|
|||||||
try:
|
try:
|
||||||
apps.remove(app_name)
|
apps.remove(app_name)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
return
|
||||||
os.makedirs(config.DATA_DIR, exist_ok=True)
|
os.makedirs(config.DATA_DIR, exist_ok=True)
|
||||||
with open(config.AUTOSTART_FILE, 'w') as f:
|
with open(config.AUTOSTART_FILE, 'w') as f:
|
||||||
for app in apps:
|
for app in apps:
|
||||||
|
@ -1,23 +1,38 @@
|
|||||||
import configparser
|
import json
|
||||||
import os
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
CONFIG_FILE = '/etc/spoc/spoc.conf'
|
DATA_DIR = '/var/lib/spoc'
|
||||||
|
AUTOSTART_FILE = '/var/lib/spoc/autostart'
|
||||||
LOCK_FILE = '/run/lock/spoc.lock'
|
LOCK_FILE = '/run/lock/spoc.lock'
|
||||||
|
|
||||||
DATA_DIR = None
|
REGISTRY_HOST = None
|
||||||
AUTOSTART_FILE = None
|
REGISTRY_AUTH = None
|
||||||
REPO_BASE_URL = None
|
REGISTRY_AUTH_FILE = '/var/lib/spoc/auth.json'
|
||||||
REPO_FILE_URL = None
|
REPO_FILE_URL = None
|
||||||
|
|
||||||
def reload(config_file=CONFIG_FILE):
|
def read_auth():
|
||||||
global DATA_DIR, AUTOSTART_FILE, REPO_BASE_URL, REPO_FILE_URL
|
global REGISTRY_HOST, REGISTRY_AUTH, REPO_FILE_URL # pylint: disable=global-statement
|
||||||
|
|
||||||
config = configparser.ConfigParser()
|
try:
|
||||||
config.read(config_file)
|
with open(REGISTRY_AUTH_FILE) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
REGISTRY_HOST = next(iter(data['auths'].keys()))
|
||||||
|
auth = b64decode(data['auths'][REGISTRY_HOST]['auth'].encode()).decode()
|
||||||
|
REGISTRY_AUTH = tuple(auth.split(':', 1))
|
||||||
|
except FileNotFoundError:
|
||||||
|
REGISTRY_HOST = 'localhost'
|
||||||
|
REGISTRY_AUTH = None
|
||||||
|
REPO_FILE_URL = f'https://{REGISTRY_HOST}/repository.json'
|
||||||
|
|
||||||
DATA_DIR = config.get('spoc', 'data-dir', fallback='/var/lib/spoc')
|
def write_auth(host, username, password):
|
||||||
AUTOSTART_FILE = os.path.join(DATA_DIR, 'autostart')
|
global REGISTRY_HOST, REGISTRY_AUTH, REPO_FILE_URL # pylint: disable=global-statement
|
||||||
REPO_BASE_URL = config.get('spoc', 'repo-url', fallback='https://localhost').rstrip('/')
|
|
||||||
REPO_FILE_URL = f'{REPO_BASE_URL}/repository.json'
|
|
||||||
|
|
||||||
reload()
|
b64auth = b64encode(f'{username}:{password}'.encode()).decode()
|
||||||
|
data = json.dumps({'auths': {host: {'auth': b64auth}}})
|
||||||
|
with open(REGISTRY_AUTH_FILE, 'w') as f:
|
||||||
|
f.write(data)
|
||||||
|
REGISTRY_HOST = host
|
||||||
|
REGISTRY_AUTH = (username, password)
|
||||||
|
REPO_FILE_URL = f'https://{REGISTRY_HOST}/repository.json'
|
||||||
|
|
||||||
|
read_auth()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
class CircularDependencyError(Exception):
|
class CircularDependencyError(Exception):
|
||||||
# Dependecy solver has found a circular dependency between items
|
# Dependecy solver has found a circular dependency between items
|
||||||
def __init__(self, deps):
|
def __init__(self, deps):
|
||||||
|
super().__init__(deps)
|
||||||
self.deps = deps
|
self.deps = deps
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -12,6 +13,7 @@ class CircularDependencyError(Exception):
|
|||||||
class MissingDependencyError(Exception):
|
class MissingDependencyError(Exception):
|
||||||
# Dependecy solver has found an items depents on a nonexistent item
|
# Dependecy solver has found an items depents on a nonexistent item
|
||||||
def __init__(self, deps, missing):
|
def __init__(self, deps, missing):
|
||||||
|
super().__init__(deps, missing)
|
||||||
self.deps = deps
|
self.deps = deps
|
||||||
self.missing = missing
|
self.missing = missing
|
||||||
|
|
||||||
@ -30,7 +32,6 @@ class DepSolver:
|
|||||||
def add(self, item, dependencies):
|
def add(self, item, dependencies):
|
||||||
self.unresolved[item] = set(dependencies)
|
self.unresolved[item] = set(dependencies)
|
||||||
|
|
||||||
#flat_list = [item for sublist in t for item in sublist]
|
|
||||||
def solve(self):
|
def solve(self):
|
||||||
# Returns a list of instances ordered by dependency
|
# Returns a list of instances ordered by dependency
|
||||||
resolved = []
|
resolved = []
|
||||||
@ -44,9 +45,8 @@ class DepSolver:
|
|||||||
missing_deps = wanted_deps - set(self.unresolved)
|
missing_deps = wanted_deps - set(self.unresolved)
|
||||||
if missing_deps:
|
if missing_deps:
|
||||||
raise MissingDependencyError(self.unresolved, missing_deps)
|
raise MissingDependencyError(self.unresolved, missing_deps)
|
||||||
else:
|
# If all dependencies exist, we have found a circular dependency
|
||||||
# If all dependencies exist, we have found a circular dependency
|
raise CircularDependencyError(self.unresolved)
|
||||||
raise CircularDependencyError(self.unresolved)
|
|
||||||
# Add resolved items to the result and remove from the unresolved ones
|
# Add resolved items to the result and remove from the unresolved ones
|
||||||
for item in batch:
|
for item in batch:
|
||||||
resolved.append(item)
|
resolved.append(item)
|
||||||
|
@ -2,30 +2,39 @@ import errno
|
|||||||
import fcntl
|
import fcntl
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import sys
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
def print_lock(pid):
|
||||||
|
with open(os.path.join('/proc', pid, 'cmdline')) as f:
|
||||||
|
cmdline = f.read().replace('\0', ' ').strip()
|
||||||
|
print(f'Waiting for lock currently held by process {pid} - {cmdline}', file=sys.stderr)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def lock(lock_file, fail_callback=None):
|
def locked():
|
||||||
with open(lock_file, 'a'):
|
with open(config.LOCK_FILE, 'a'):
|
||||||
# Open the lock file in append mode first to ensure its existence
|
# Open the lock file in append mode first to ensure its existence
|
||||||
# but not modify any data if it already exists
|
# but not modify any data if it already exists
|
||||||
pass
|
pass
|
||||||
# Open the lock file in read + write mode without truncation
|
# Open the lock file in read + write mode without truncation
|
||||||
with open(lock_file, 'r+') as f:
|
with open(config.LOCK_FILE, 'r+') as f:
|
||||||
|
lock_printed = False
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
# Try to obtain exclusive lock in non-blocking mode
|
# Try to obtain exclusive lock in non-blocking mode
|
||||||
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
break
|
break
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
# If lock is already locked by another process
|
# If lock is held by another process
|
||||||
if e.errno == errno.EAGAIN:
|
if e.errno == errno.EAGAIN:
|
||||||
if fail_callback:
|
if not lock_printed:
|
||||||
# Call the callback function with contents of the lock file
|
# Print a message using contents of the lock file
|
||||||
# (PID of the process holding the lock)
|
# (PID of the process holding the lock)
|
||||||
fail_callback(f.read())
|
print_lock(f.read())
|
||||||
# Remove the callback function so it's not called in every loop
|
# Set flag so the message is not printed in every loop
|
||||||
fail_callback = None
|
lock_printed = True
|
||||||
# Set the position for future truncation
|
# Set the position for future truncation
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
# Wait for the lock to be freed
|
# Wait for the lock to be freed
|
||||||
@ -38,12 +47,3 @@ def lock(lock_file, fail_callback=None):
|
|||||||
f.write(str(os.getpid()))
|
f.write(str(os.getpid()))
|
||||||
f.flush()
|
f.flush()
|
||||||
yield f
|
yield f
|
||||||
|
|
||||||
# Function decorator
|
|
||||||
def locked(lock_file, fail_callback=None):
|
|
||||||
def decorator(target):
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
with lock(lock_file, fail_callback):
|
|
||||||
return target(*args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
return decorator
|
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from . import config
|
||||||
|
|
||||||
|
ENV = os.environ.copy()
|
||||||
|
ENV['REGISTRY_AUTH_FILE'] = config.REGISTRY_AUTH_FILE
|
||||||
|
|
||||||
|
def run(cmd, **kwargs):
|
||||||
|
return subprocess.run(cmd, check=True, env=ENV, **kwargs)
|
||||||
|
|
||||||
|
def out(cmd, **kwargs):
|
||||||
|
return run(cmd, stdout=subprocess.PIPE, text=True, **kwargs).stdout.rstrip()
|
||||||
|
|
||||||
def get_apps():
|
def get_apps():
|
||||||
apps = {}
|
apps = {}
|
||||||
cmd = ['podman', 'pod', 'ps', '--format', 'json']
|
data = json.loads(out(['podman', 'pod', 'ps', '--format', 'json']))
|
||||||
pod_ps = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
|
|
||||||
data = json.loads(pod_ps.stdout)
|
|
||||||
for pod in data:
|
for pod in data:
|
||||||
app_name = pod['Labels'].get('spoc.app')
|
app_name = pod['Labels'].get('spoc.app')
|
||||||
app_version = pod['Labels'].get('spoc.version')
|
app_version = pod['Labels'].get('spoc.version')
|
||||||
@ -14,42 +24,35 @@ def get_apps():
|
|||||||
return apps
|
return apps
|
||||||
|
|
||||||
def get_volumes_for_app(app_name):
|
def get_volumes_for_app(app_name):
|
||||||
cmd = ['podman', 'volume', 'ls', '--filter', f'label=spoc.app={app_name}', '--format', 'json']
|
volume_ls = out(['podman', 'volume', 'ls', '--filter', f'label=spoc.app={app_name}',
|
||||||
volume_ls = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
|
'--format', 'json'])
|
||||||
return set(volume['Name'] for volume in json.loads(volume_ls.stdout))
|
return set(volume['Name'] for volume in json.loads(volume_ls))
|
||||||
|
|
||||||
def start_pod(app_name):
|
def start_pod(app_name):
|
||||||
cmd = ['podman', 'pod', 'start', app_name]
|
run(['podman', 'pod', 'start', app_name])
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
def stop_pod(app_name):
|
def stop_pod(app_name):
|
||||||
cmd = ['podman', 'pod', 'stop', '--ignore', app_name]
|
run(['podman', 'pod', 'stop', '--ignore', app_name])
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
def get_pod_status(app_name=None):
|
def get_pod_status(app_name=None):
|
||||||
cmd = ['podman', 'pod', 'ps']
|
cmd = ['podman', 'pod', 'ps']
|
||||||
if app_name:
|
if app_name:
|
||||||
cmd.extend(['--filter', f'label=spoc.app={app_name}'])
|
cmd.extend(['--filter', f'label=spoc.app={app_name}'])
|
||||||
pod_ps = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, text=True)
|
return out(cmd)
|
||||||
return pod_ps.stdout.strip()
|
|
||||||
|
|
||||||
def create_volume(app_name, vol_name):
|
def create_volume(app_name, vol_name):
|
||||||
cmd = ['podman', 'volume', 'create', '--label', f'spoc.app={app_name}', vol_name]
|
run(['podman', 'volume', 'create', '--label', f'spoc.app={app_name}', vol_name])
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
def remove_volume(vol_name):
|
def remove_volume(vol_name):
|
||||||
cmd = ['podman', 'volume', 'rm', vol_name]
|
run(['podman', 'volume', 'rm', vol_name])
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
def create_pod(app_name, app_version):
|
def create_pod(app_name, app_version):
|
||||||
cmd = ['podman', 'pod', 'create', '--name', app_name,
|
run(['podman', 'pod', 'create', '--name', app_name,
|
||||||
'--label', f'spoc.app={app_name}', '--label', f'spoc.version={app_version}']
|
'--label', f'spoc.app={app_name}', '--label', f'spoc.version={app_version}'])
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
def remove_pod(app_name):
|
def remove_pod(app_name):
|
||||||
stop_pod(app_name)
|
stop_pod(app_name)
|
||||||
cmd = ['podman', 'pod', 'rm', '--ignore', app_name]
|
run(['podman', 'pod', 'rm', '--ignore', app_name])
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
|
||||||
def create_container(app_name, cnt_name, image, env_file=None, volumes=None,
|
def create_container(app_name, cnt_name, image, env_file=None, volumes=None,
|
||||||
requires=None, hosts=None):
|
requires=None, hosts=None):
|
||||||
@ -66,10 +69,8 @@ def create_container(app_name, cnt_name, image, env_file=None, volumes=None,
|
|||||||
for host in sorted(hosts):
|
for host in sorted(hosts):
|
||||||
cmd.extend(['--add-host', f'{host}:127.0.0.1'])
|
cmd.extend(['--add-host', f'{host}:127.0.0.1'])
|
||||||
cmd.append(image)
|
cmd.append(image)
|
||||||
subprocess.run(cmd, check=True)
|
run(cmd)
|
||||||
|
|
||||||
def prune():
|
def prune():
|
||||||
cmd = ['podman', 'image', 'prune', '--all', '--force']
|
run(['podman', 'image', 'prune', '--all', '--force'])
|
||||||
subprocess.run(cmd, check=True)
|
run(['podman', 'volume', 'prune', '--force'])
|
||||||
cmd = ['podman', 'volume', 'prune', '--force']
|
|
||||||
subprocess.run(cmd, check=True)
|
|
||||||
|
@ -5,9 +5,10 @@ from . import config
|
|||||||
_data = {}
|
_data = {}
|
||||||
|
|
||||||
def load(force=False):
|
def load(force=False):
|
||||||
global _data
|
global _data # pylint: disable=global-statement
|
||||||
if not _data or force:
|
if not _data or force:
|
||||||
response = requests.get(config.REPO_FILE_URL, timeout=5)
|
_data = {}
|
||||||
|
response = requests.get(config.REPO_FILE_URL, auth=config.REGISTRY_AUTH, timeout=5)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
_data = response.json()
|
_data = response.json()
|
||||||
|
|
||||||
|
168
src/spoc_cli.py
Normal file
168
src/spoc_cli.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
import argparse
|
||||||
|
import getpass
|
||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import spoc
|
||||||
|
|
||||||
|
APP_ERROR_STRINGS = {
|
||||||
|
'AppAlreadyInstalledError': 'Application {} is already installed',
|
||||||
|
'AppNotInstalledError': 'Application {} is not installed',
|
||||||
|
'AppNotInRepoError': 'Application {} does not exist in the repository',
|
||||||
|
'AppNotUpdateableError': 'Application {} does not have a newer version to update',
|
||||||
|
}
|
||||||
|
|
||||||
|
def handle_app_error(exception):
|
||||||
|
ex_type = type(exception).__name__
|
||||||
|
print(APP_ERROR_STRINGS[ex_type].format(exception.app_name), file=sys.stderr)
|
||||||
|
|
||||||
|
def handle_repo_error(exception):
|
||||||
|
if isinstance(exception, requests.HTTPError):
|
||||||
|
status = exception.response.status_code
|
||||||
|
if status == 401:
|
||||||
|
reason = 'Invalid username/password'
|
||||||
|
else:
|
||||||
|
reason = f'{status} {exception.response.reason}'
|
||||||
|
else:
|
||||||
|
ex_type = type(exception)
|
||||||
|
reason = f'{ex_type.__module__}.{ex_type.__name__}'
|
||||||
|
print(f'Online repository cannot be reached due to: {reason}', file=sys.stderr)
|
||||||
|
|
||||||
|
def listing(list_type):
|
||||||
|
if list_type == 'installed':
|
||||||
|
apps = spoc.list_installed()
|
||||||
|
elif list_type == 'online':
|
||||||
|
apps = spoc.list_online()
|
||||||
|
elif list_type == 'updates':
|
||||||
|
apps = spoc.list_updates()
|
||||||
|
else:
|
||||||
|
apps = {}
|
||||||
|
for app_name, app_version in apps.items():
|
||||||
|
print(app_name, app_version)
|
||||||
|
|
||||||
|
def install(app_name):
|
||||||
|
spoc.install(app_name)
|
||||||
|
|
||||||
|
def update(app_name):
|
||||||
|
spoc.update(app_name)
|
||||||
|
|
||||||
|
def uninstall(app_name):
|
||||||
|
spoc.uninstall(app_name)
|
||||||
|
|
||||||
|
def start(app_name):
|
||||||
|
spoc.start(app_name)
|
||||||
|
|
||||||
|
def stop(app_name):
|
||||||
|
spoc.stop(app_name)
|
||||||
|
|
||||||
|
def status(app_name):
|
||||||
|
print(spoc.status(app_name))
|
||||||
|
|
||||||
|
def set_autostart(app_name, value):
|
||||||
|
enabled = value.lower() in ('1', 'on', 'enable', 'true')
|
||||||
|
spoc.set_autostart(app_name, enabled)
|
||||||
|
|
||||||
|
def start_autostarted():
|
||||||
|
spoc.start_autostarted()
|
||||||
|
|
||||||
|
def stop_all():
|
||||||
|
spoc.stop_all()
|
||||||
|
|
||||||
|
def login(host):
|
||||||
|
username = input('Username: ')
|
||||||
|
password = getpass.getpass()
|
||||||
|
spoc.login(host, username, password)
|
||||||
|
|
||||||
|
def prune():
|
||||||
|
spoc.prune()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(args=None):
|
||||||
|
parser = argparse.ArgumentParser(description='SPOC application manager')
|
||||||
|
parser.set_defaults(action=None)
|
||||||
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
|
parser_list = subparsers.add_parser('list')
|
||||||
|
parser_list.set_defaults(action=listing)
|
||||||
|
parser_list.add_argument('type', choices=('installed', 'online', 'updates'),
|
||||||
|
default='installed', const='installed', nargs='?',
|
||||||
|
help='Selected repository or application criteria')
|
||||||
|
|
||||||
|
parser_install = subparsers.add_parser('install')
|
||||||
|
parser_install.set_defaults(action=install)
|
||||||
|
parser_install.add_argument('app', help='Name of the application to install')
|
||||||
|
|
||||||
|
parser_update = subparsers.add_parser('update')
|
||||||
|
parser_update.set_defaults(action=update)
|
||||||
|
parser_update.add_argument('app', help='Name of the application to update')
|
||||||
|
|
||||||
|
parser_uninstall = subparsers.add_parser('uninstall')
|
||||||
|
parser_uninstall.set_defaults(action=uninstall)
|
||||||
|
parser_uninstall.add_argument('app', help='Name of the application to uninstall')
|
||||||
|
|
||||||
|
parser_start = subparsers.add_parser('start')
|
||||||
|
parser_start.set_defaults(action=start)
|
||||||
|
parser_start.add_argument('app', help='Name of the application to start')
|
||||||
|
|
||||||
|
parser_stop = subparsers.add_parser('stop')
|
||||||
|
parser_stop.set_defaults(action=stop)
|
||||||
|
parser_stop.add_argument('app', help='Name of the application to stop')
|
||||||
|
|
||||||
|
parser_status = subparsers.add_parser('status')
|
||||||
|
parser_status.set_defaults(action=status)
|
||||||
|
parser_status.add_argument('app', nargs='?', help='Name of the application to check')
|
||||||
|
|
||||||
|
parser_autostart = subparsers.add_parser('autostart')
|
||||||
|
parser_autostart.set_defaults(action=set_autostart)
|
||||||
|
parser_autostart.add_argument('app', help='Name of the application to be automatically started')
|
||||||
|
parser_autostart.add_argument('value', choices=('1', 'on', 'enable', 'true',
|
||||||
|
'0', 'off', 'disable', 'false'),
|
||||||
|
help='Set or unset the applications to be automatically ' \
|
||||||
|
'started after the host boots up')
|
||||||
|
|
||||||
|
parser_start_autostarted = subparsers.add_parser('start-autostarted')
|
||||||
|
parser_start_autostarted.set_defaults(action=start_autostarted)
|
||||||
|
|
||||||
|
parser_stop_all = subparsers.add_parser('stop-all')
|
||||||
|
parser_stop_all.set_defaults(action=stop_all)
|
||||||
|
|
||||||
|
parser_login = subparsers.add_parser('login')
|
||||||
|
parser_login.set_defaults(action=login)
|
||||||
|
parser_login.add_argument('host', help='Hostname of the container registry')
|
||||||
|
|
||||||
|
parser_prune = subparsers.add_parser('prune')
|
||||||
|
parser_prune.set_defaults(action=prune)
|
||||||
|
|
||||||
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
try:
|
||||||
|
if args.action is listing:
|
||||||
|
listing(args.type)
|
||||||
|
elif args.action is install:
|
||||||
|
install(args.app)
|
||||||
|
elif args.action is update:
|
||||||
|
update(args.app)
|
||||||
|
elif args.action is uninstall:
|
||||||
|
uninstall(args.app)
|
||||||
|
elif args.action is start:
|
||||||
|
start(args.app)
|
||||||
|
elif args.action is stop:
|
||||||
|
stop(args.app)
|
||||||
|
elif args.action is status:
|
||||||
|
status(args.app)
|
||||||
|
elif args.action is set_autostart:
|
||||||
|
set_autostart(args.app, args.value)
|
||||||
|
elif args.action is start_autostarted:
|
||||||
|
start_autostarted()
|
||||||
|
elif args.action is stop_all:
|
||||||
|
stop_all()
|
||||||
|
elif args.action is login:
|
||||||
|
login(args.host)
|
||||||
|
elif args.action is prune:
|
||||||
|
prune()
|
||||||
|
except spoc.AppError as ex:
|
||||||
|
handle_app_error(ex)
|
||||||
|
except requests.RequestException as ex:
|
||||||
|
handle_repo_error(ex)
|
@ -10,11 +10,15 @@ TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
|||||||
with open(os.path.join(TEST_DATA_DIR, 'repository.json')) as f:
|
with open(os.path.join(TEST_DATA_DIR, 'repository.json')) as f:
|
||||||
MOCK_REPODATA = json.load(f)
|
MOCK_REPODATA = json.load(f)
|
||||||
|
|
||||||
with open(os.path.join(TEST_DATA_DIR, 'test.env')) as f:
|
MOCK_ENV = 'RAILS_ENV=test\n' \
|
||||||
MOCK_ENV = f.read()
|
'POSTGRES_PASSWORD=asdf=1234\n' \
|
||||||
|
'SOMEKEY=someval\n'
|
||||||
|
|
||||||
with open(os.path.join(TEST_DATA_DIR, 'test.env.json')) as f:
|
MOCK_ENV_DATA = {
|
||||||
MOCK_ENV_JSON = json.load(f)
|
'RAILS_ENV': 'test',
|
||||||
|
'POSTGRES_PASSWORD': 'asdf=1234',
|
||||||
|
'SOMEKEY': 'someval',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_init():
|
def test_init():
|
||||||
@ -49,7 +53,7 @@ def test_install(create_containers, create_pod, write_env_vars, read_env_vars, c
|
|||||||
@patch('spoc.app.App.get_existing_volumes', return_value=set(('somevol', 'migrate', 'storage')))
|
@patch('spoc.app.App.get_existing_volumes', return_value=set(('somevol', 'migrate', 'storage')))
|
||||||
@patch('spoc.app.App.remove_volumes')
|
@patch('spoc.app.App.remove_volumes')
|
||||||
@patch('spoc.app.App.create_volumes')
|
@patch('spoc.app.App.create_volumes')
|
||||||
@patch('spoc.app.App.read_env_vars', return_value=MOCK_ENV_JSON)
|
@patch('spoc.app.App.read_env_vars', return_value=MOCK_ENV_DATA)
|
||||||
@patch('spoc.app.App.write_env_vars')
|
@patch('spoc.app.App.write_env_vars')
|
||||||
@patch('spoc.app.App.create_pod')
|
@patch('spoc.app.App.create_pod')
|
||||||
@patch('spoc.app.App.create_containers')
|
@patch('spoc.app.App.create_containers')
|
||||||
@ -64,20 +68,22 @@ def test_update(create_containers, create_pod, write_env_vars, read_env_vars, cr
|
|||||||
create_volumes.assert_called_once_with(set(('uploads', 'postgres-data')))
|
create_volumes.assert_called_once_with(set(('uploads', 'postgres-data')))
|
||||||
read_env_vars.assert_called_once()
|
read_env_vars.assert_called_once()
|
||||||
expected_env_data = MOCK_REPODATA['someapp']['environment'].copy()
|
expected_env_data = MOCK_REPODATA['someapp']['environment'].copy()
|
||||||
expected_env_data.update(MOCK_ENV_JSON)
|
expected_env_data.update(MOCK_ENV_DATA)
|
||||||
del expected_env_data['SOMEKEY']
|
del expected_env_data['SOMEKEY']
|
||||||
write_env_vars.assert_called_once_with(expected_env_data)
|
write_env_vars.assert_called_once_with(expected_env_data)
|
||||||
create_pod.assert_called_once_with('0.23.5-210416')
|
create_pod.assert_called_once_with('0.23.5-210416')
|
||||||
create_containers.assert_called_once_with(MOCK_REPODATA['someapp']['containers'])
|
create_containers.assert_called_once_with(MOCK_REPODATA['someapp']['containers'])
|
||||||
|
|
||||||
|
@patch('spoc.autostart.set_app')
|
||||||
@patch('spoc.app.App.remove_pod')
|
@patch('spoc.app.App.remove_pod')
|
||||||
@patch('spoc.app.App.remove_env_vars')
|
@patch('spoc.app.App.remove_env_vars')
|
||||||
@patch('spoc.app.App.get_existing_volumes', return_value=set(('somevol', 'anothervol')))
|
@patch('spoc.app.App.get_existing_volumes', return_value=set(('somevol', 'anothervol')))
|
||||||
@patch('spoc.app.App.remove_volumes')
|
@patch('spoc.app.App.remove_volumes')
|
||||||
def test_uninstall(remove_volumes, get_existing_volumes, remove_env_vars, remove_pod):
|
def test_uninstall(remove_volumes, get_existing_volumes, remove_env_vars, remove_pod, autostart):
|
||||||
instance = app.App('someapp')
|
instance = app.App('someapp')
|
||||||
instance.uninstall()
|
instance.uninstall()
|
||||||
|
|
||||||
|
autostart.assert_called_with('someapp', False)
|
||||||
remove_pod.assert_called_once()
|
remove_pod.assert_called_once()
|
||||||
remove_env_vars.assert_called_once()
|
remove_env_vars.assert_called_once()
|
||||||
get_existing_volumes.assert_called_once()
|
get_existing_volumes.assert_called_once()
|
||||||
@ -106,7 +112,7 @@ def test_read_env_vars(env_open):
|
|||||||
|
|
||||||
env_file = os.path.join(config.DATA_DIR, 'someapp.env')
|
env_file = os.path.join(config.DATA_DIR, 'someapp.env')
|
||||||
env_open.assert_called_once_with(env_file)
|
env_open.assert_called_once_with(env_file)
|
||||||
assert env_vars == MOCK_ENV_JSON
|
assert env_vars == MOCK_ENV_DATA
|
||||||
|
|
||||||
@patch('builtins.open', side_effect=FileNotFoundError('someapp.env'))
|
@patch('builtins.open', side_effect=FileNotFoundError('someapp.env'))
|
||||||
def test_read_env_vars_filenotfound(env_open):
|
def test_read_env_vars_filenotfound(env_open):
|
||||||
@ -121,7 +127,7 @@ def test_read_env_vars_filenotfound(env_open):
|
|||||||
@patch('builtins.open', new_callable=mock_open)
|
@patch('builtins.open', new_callable=mock_open)
|
||||||
def test_write_env_vars(env_open, makedirs):
|
def test_write_env_vars(env_open, makedirs):
|
||||||
instance = app.App('someapp')
|
instance = app.App('someapp')
|
||||||
instance.write_env_vars(MOCK_ENV_JSON)
|
instance.write_env_vars(MOCK_ENV_DATA)
|
||||||
|
|
||||||
makedirs.assert_called_once_with(config.DATA_DIR, exist_ok=True)
|
makedirs.assert_called_once_with(config.DATA_DIR, exist_ok=True)
|
||||||
env_file = os.path.join(config.DATA_DIR, 'someapp.env')
|
env_file = os.path.join(config.DATA_DIR, 'someapp.env')
|
||||||
|
@ -51,8 +51,6 @@ def test_set_app_nonexistent(file_open, get_apps, makedirs):
|
|||||||
autostart.set_app('anotherapp', False)
|
autostart.set_app('anotherapp', False)
|
||||||
|
|
||||||
get_apps.assert_called_once()
|
get_apps.assert_called_once()
|
||||||
makedirs.assert_called_once_with(config.DATA_DIR, exist_ok=True)
|
makedirs.assert_not_called()
|
||||||
file_open.assert_called_once_with(config.AUTOSTART_FILE, 'w')
|
file_open.assert_not_called()
|
||||||
file_open().write.assert_has_calls([
|
file_open().write.assert_not_called()
|
||||||
call('someapp\n'),
|
|
||||||
])
|
|
||||||
|
343
tests/test_cli.py
Normal file
343
tests/test_cli.py
Normal file
@ -0,0 +1,343 @@
|
|||||||
|
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
from argparse import Namespace
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import spoc
|
||||||
|
import spoc_cli
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
def __init__(self, status_code, reason):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('exception,expected',[
|
||||||
|
(spoc.AppAlreadyInstalledError('someapp'),
|
||||||
|
'Application someapp is already installed\n'),
|
||||||
|
(spoc.AppNotInstalledError('someapp'),
|
||||||
|
'Application someapp is not installed\n'),
|
||||||
|
(spoc.AppNotInRepoError('someapp'),
|
||||||
|
'Application someapp does not exist in the repository\n'),
|
||||||
|
(spoc.AppNotUpdateableError('someapp'),
|
||||||
|
'Application someapp does not have a newer version to update\n'),
|
||||||
|
])
|
||||||
|
def test_handle_repo_error(exception, expected, capsys):
|
||||||
|
spoc_cli.handle_app_error(exception)
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.err == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('exception,expected',[
|
||||||
|
(requests.HTTPError(response=MockResponse(401, 'Unauthorized')),
|
||||||
|
'Invalid username/password'),
|
||||||
|
(requests.HTTPError(response=MockResponse(404, 'Not Found')),
|
||||||
|
'404 Not Found'),
|
||||||
|
(requests.ConnectTimeout(),
|
||||||
|
'requests.exceptions.ConnectTimeout'),
|
||||||
|
])
|
||||||
|
def test_handle_app_error(exception, expected, capsys):
|
||||||
|
spoc_cli.handle_repo_error(exception)
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
expected = f'Online repository cannot be reached due to: {expected}\n'
|
||||||
|
assert captured.err == expected
|
||||||
|
|
||||||
|
@patch('spoc.list_installed', return_value={'anotherapp': '0.1', 'someapp': '0.1'})
|
||||||
|
@patch('spoc.list_online')
|
||||||
|
@patch('spoc.list_updates')
|
||||||
|
def test_listing_installed(list_updates, list_online, list_installed, capsys):
|
||||||
|
spoc_cli.listing('installed')
|
||||||
|
|
||||||
|
list_installed.assert_called_once()
|
||||||
|
list_online.assert_not_called()
|
||||||
|
list_updates.assert_not_called()
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.out == 'anotherapp 0.1\nsomeapp 0.1\n'
|
||||||
|
|
||||||
|
@patch('spoc.list_installed')
|
||||||
|
@patch('spoc.list_online', return_value={'anotherapp': '0.2', 'someapp': '0.2'})
|
||||||
|
@patch('spoc.list_updates')
|
||||||
|
def test_listing_online(list_updates, list_online, list_installed, capsys):
|
||||||
|
spoc_cli.listing('online')
|
||||||
|
|
||||||
|
list_installed.assert_not_called()
|
||||||
|
list_online.assert_called_once()
|
||||||
|
list_updates.assert_not_called()
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.out == 'anotherapp 0.2\nsomeapp 0.2\n'
|
||||||
|
|
||||||
|
@patch('spoc.list_installed')
|
||||||
|
@patch('spoc.list_online')
|
||||||
|
@patch('spoc.list_updates', return_value={'anotherapp': '0.1 -> 0.2', 'someapp': '0.1 -> 0.2'})
|
||||||
|
def test_listing_updates(list_updates, list_online, list_installed, capsys):
|
||||||
|
spoc_cli.listing('updates')
|
||||||
|
|
||||||
|
list_installed.assert_not_called()
|
||||||
|
list_online.assert_not_called()
|
||||||
|
list_updates.assert_called_once()
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.out == 'anotherapp 0.1 -> 0.2\nsomeapp 0.1 -> 0.2\n'
|
||||||
|
|
||||||
|
@patch('spoc.list_installed')
|
||||||
|
@patch('spoc.list_online')
|
||||||
|
@patch('spoc.list_updates')
|
||||||
|
def test_listing_invalid(list_updates, list_online, list_installed, capsys):
|
||||||
|
spoc_cli.listing('invalid')
|
||||||
|
|
||||||
|
list_installed.assert_not_called()
|
||||||
|
list_online.assert_not_called()
|
||||||
|
list_updates.assert_not_called()
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert not captured.out
|
||||||
|
|
||||||
|
@patch('spoc.install')
|
||||||
|
def test_install(install):
|
||||||
|
spoc_cli.install('someapp')
|
||||||
|
|
||||||
|
install.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('spoc.update')
|
||||||
|
def test_update(update):
|
||||||
|
spoc_cli.update('someapp')
|
||||||
|
|
||||||
|
update.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('spoc.uninstall')
|
||||||
|
def test_uninstall(uninstall):
|
||||||
|
spoc_cli.uninstall('someapp')
|
||||||
|
|
||||||
|
uninstall.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('spoc.start')
|
||||||
|
def test_start(start):
|
||||||
|
spoc_cli.start('someapp')
|
||||||
|
|
||||||
|
start.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('spoc.stop')
|
||||||
|
def test_stop(stop):
|
||||||
|
spoc_cli.stop('someapp')
|
||||||
|
|
||||||
|
stop.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('spoc.status', return_value='STATUS')
|
||||||
|
def test_status(status, capsys):
|
||||||
|
spoc_cli.status('someapp')
|
||||||
|
|
||||||
|
status.assert_called_once_with('someapp')
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.out == 'STATUS\n'
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('value,expected',[
|
||||||
|
('1', True),
|
||||||
|
('on', True),
|
||||||
|
('Enable', True),
|
||||||
|
('TRUE', True),
|
||||||
|
('0', False),
|
||||||
|
('off', False),
|
||||||
|
('Disable', False),
|
||||||
|
('FALSE', False),
|
||||||
|
('whatever', False),
|
||||||
|
])
|
||||||
|
@patch('spoc.set_autostart')
|
||||||
|
def test_set_autostart(set_autostart, value, expected):
|
||||||
|
spoc_cli.set_autostart('someapp', value)
|
||||||
|
|
||||||
|
set_autostart.assert_called_once_with('someapp', expected)
|
||||||
|
|
||||||
|
@patch('spoc.start_autostarted')
|
||||||
|
def test_start_autostarted(start_autostarted):
|
||||||
|
spoc_cli.start_autostarted()
|
||||||
|
|
||||||
|
start_autostarted.assert_called_once()
|
||||||
|
|
||||||
|
@patch('spoc.stop_all')
|
||||||
|
def test_stop_all(stop_all):
|
||||||
|
spoc_cli.stop_all()
|
||||||
|
|
||||||
|
stop_all.assert_called_once()
|
||||||
|
|
||||||
|
@patch('builtins.input', return_value='someuser')
|
||||||
|
@patch('getpass.getpass', return_value='somepass')
|
||||||
|
@patch('spoc.login')
|
||||||
|
def test_login(login, getpass, nput):
|
||||||
|
spoc_cli.login('somehost')
|
||||||
|
|
||||||
|
nput.assert_called_once_with('Username: ')
|
||||||
|
getpass.assert_called_once()
|
||||||
|
login.assert_called_once_with('somehost', 'someuser', 'somepass')
|
||||||
|
|
||||||
|
@patch('spoc.prune')
|
||||||
|
def test_prune(prune):
|
||||||
|
spoc_cli.prune()
|
||||||
|
|
||||||
|
prune.assert_called_once()
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'list'])
|
||||||
|
@patch('spoc_cli.listing')
|
||||||
|
def test_main_listing(listing):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
listing.assert_called_once_with('installed')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'list', 'online'])
|
||||||
|
@patch('spoc_cli.listing')
|
||||||
|
def test_main_listing_online(listing):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
listing.assert_called_once_with('online')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'install', 'someapp'])
|
||||||
|
@patch('spoc_cli.install')
|
||||||
|
def test_main_install(install):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
install.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'update', 'someapp'])
|
||||||
|
@patch('spoc_cli.update')
|
||||||
|
def test_main_update(update):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
update.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'uninstall', 'someapp'])
|
||||||
|
@patch('spoc_cli.uninstall')
|
||||||
|
def test_main_uninstall(uninstall):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
uninstall.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'start', 'someapp'])
|
||||||
|
@patch('spoc_cli.start')
|
||||||
|
def test_main_start(start):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
start.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'stop', 'someapp'])
|
||||||
|
@patch('spoc_cli.stop')
|
||||||
|
def test_main_stop(stop):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
stop.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'status', 'someapp'])
|
||||||
|
@patch('spoc_cli.status')
|
||||||
|
def test_main_status(status):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
status.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'status'])
|
||||||
|
@patch('spoc_cli.status')
|
||||||
|
def test_main_status_all(status):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
status.assert_called_once_with(None)
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'autostart', 'someapp', 'on'])
|
||||||
|
@patch('spoc_cli.set_autostart')
|
||||||
|
def test_main_autostart(autostart):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
autostart.assert_called_once_with('someapp', 'on')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'start-autostarted'])
|
||||||
|
@patch('spoc_cli.start_autostarted')
|
||||||
|
def test_main_start_autostarted(start_autostarted):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
start_autostarted.assert_called_once()
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'stop-all'])
|
||||||
|
@patch('spoc_cli.stop_all')
|
||||||
|
def test_main_stop_all(stop_all):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
stop_all.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'login', 'example.com'])
|
||||||
|
@patch('spoc_cli.login')
|
||||||
|
def test_main_login(login):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
login.assert_called_once_with('example.com')
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'prune'])
|
||||||
|
@patch('spoc_cli.prune')
|
||||||
|
def test_main_prune(prune):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
prune.assert_called_once()
|
||||||
|
|
||||||
|
@patch('spoc_cli.parse_args', return_value=Namespace(action=None))
|
||||||
|
@patch('spoc_cli.listing')
|
||||||
|
@patch('spoc_cli.install')
|
||||||
|
@patch('spoc_cli.update')
|
||||||
|
@patch('spoc_cli.uninstall')
|
||||||
|
@patch('spoc_cli.start')
|
||||||
|
@patch('spoc_cli.stop')
|
||||||
|
@patch('spoc_cli.status')
|
||||||
|
@patch('spoc_cli.start_autostarted')
|
||||||
|
@patch('spoc_cli.stop_all')
|
||||||
|
@patch('spoc_cli.login')
|
||||||
|
@patch('spoc_cli.prune')
|
||||||
|
def test_main_invalid(prune, login, stop_all, start_autostarted, status, stop, start,
|
||||||
|
uninstall, update, install, listing, parse_args):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
parse_args.assert_called_once()
|
||||||
|
listing.assert_not_called()
|
||||||
|
install.assert_not_called()
|
||||||
|
update.assert_not_called()
|
||||||
|
uninstall.assert_not_called()
|
||||||
|
start.assert_not_called()
|
||||||
|
stop.assert_not_called()
|
||||||
|
status.assert_not_called()
|
||||||
|
start_autostarted.assert_not_called()
|
||||||
|
stop_all.assert_not_called()
|
||||||
|
login.assert_not_called()
|
||||||
|
prune.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('argv', [
|
||||||
|
['list', 'invalid'],
|
||||||
|
['install'],
|
||||||
|
['update'],
|
||||||
|
['uninstall'],
|
||||||
|
['start'],
|
||||||
|
['stop'],
|
||||||
|
['autostart'],
|
||||||
|
['autostart', 'someapp'],
|
||||||
|
['login'],
|
||||||
|
['invalid'],
|
||||||
|
])
|
||||||
|
def test_main_systemexit(argv):
|
||||||
|
argv.insert(0, 'foo')
|
||||||
|
with patch('sys.argv', argv):
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'start', 'someapp'])
|
||||||
|
@patch('spoc_cli.start', side_effect=spoc.AppNotInstalledError('someapp'))
|
||||||
|
@patch('spoc_cli.handle_app_error')
|
||||||
|
def test_main_apperror(handle_app_error, start):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
start.assert_called_once()
|
||||||
|
handle_app_error.assert_called_once()
|
||||||
|
|
||||||
|
@patch('sys.argv', ['foo', 'login', 'somehost'])
|
||||||
|
@patch('spoc_cli.login', side_effect=requests.HTTPError(response=MockResponse(401, 'Unauthorized')))
|
||||||
|
@patch('spoc_cli.handle_repo_error')
|
||||||
|
def test_main_repoerror(handle_repo_error, login):
|
||||||
|
spoc_cli.main()
|
||||||
|
|
||||||
|
login.assert_called_once()
|
||||||
|
handle_repo_error.assert_called_once()
|
@ -1,25 +1,34 @@
|
|||||||
import os
|
from unittest.mock import mock_open, patch
|
||||||
|
|
||||||
from spoc import config
|
from spoc import config
|
||||||
|
|
||||||
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
@patch('builtins.open', new_callable=mock_open,
|
||||||
with open(os.path.join(TEST_DATA_DIR, 'spoc.conf')) as f:
|
read_data='{"auths": {"example.com": {"auth": "c29tZXVzZXI6c29tZXBhc3N3b3Jk"}}}')
|
||||||
MOCK_CONFIG = f.read()
|
def test_read_auth(auth_open):
|
||||||
|
config.read_auth()
|
||||||
|
|
||||||
def test_config():
|
auth_open.assert_called_once_with(config.REGISTRY_AUTH_FILE)
|
||||||
config_file = os.path.join(os.path.dirname(__file__), 'test_data/spoc.conf')
|
assert config.REGISTRY_HOST == 'example.com'
|
||||||
config.reload(config_file)
|
assert config.REGISTRY_AUTH == ('someuser', 'somepassword')
|
||||||
|
assert config.REPO_FILE_URL == 'https://example.com/repository.json'
|
||||||
|
|
||||||
assert config.DATA_DIR == '/some/data/dir'
|
@patch('builtins.open', side_effect=FileNotFoundError('auth.json'))
|
||||||
assert config.AUTOSTART_FILE == '/some/data/dir/autostart'
|
def test_read_auth_fallback(auth_open):
|
||||||
assert config.REPO_BASE_URL == 'https://user:pass@example.com/spoc'
|
config.read_auth()
|
||||||
assert config.REPO_FILE_URL == 'https://user:pass@example.com/spoc/repository.json'
|
|
||||||
|
|
||||||
def test_default_config():
|
auth_open.assert_called_once_with(config.REGISTRY_AUTH_FILE)
|
||||||
config_file = os.path.join(os.path.dirname(__file__), 'test_data/nonexistent')
|
assert config.REGISTRY_HOST == 'localhost'
|
||||||
config.reload(config_file)
|
assert config.REGISTRY_AUTH is None
|
||||||
|
|
||||||
assert config.DATA_DIR == '/var/lib/spoc'
|
|
||||||
assert config.AUTOSTART_FILE == '/var/lib/spoc/autostart'
|
|
||||||
assert config.REPO_BASE_URL == 'https://localhost'
|
|
||||||
assert config.REPO_FILE_URL == 'https://localhost/repository.json'
|
assert config.REPO_FILE_URL == 'https://localhost/repository.json'
|
||||||
|
|
||||||
|
@patch('builtins.open', new_callable=mock_open)
|
||||||
|
def test_write_auth(auth_open):
|
||||||
|
config.write_auth('example.org', 'user', 'anotherpwd')
|
||||||
|
|
||||||
|
auth_open.assert_called_once_with(config.REGISTRY_AUTH_FILE, 'w')
|
||||||
|
expected_data = '{"auths": {"example.org": {"auth": "dXNlcjphbm90aGVycHdk"}}}'
|
||||||
|
auth_open().write.assert_called_once_with(expected_data)
|
||||||
|
|
||||||
|
assert config.REGISTRY_HOST == 'example.org'
|
||||||
|
assert config.REGISTRY_AUTH == ('user', 'anotherpwd')
|
||||||
|
assert config.REPO_FILE_URL == 'https://example.org/repository.json'
|
||||||
|
@ -13,19 +13,8 @@
|
|||||||
"POSTGRES_USER": "someapp",
|
"POSTGRES_USER": "someapp",
|
||||||
"POSTGRES_PASSWORD": "someapp",
|
"POSTGRES_PASSWORD": "someapp",
|
||||||
"POSTGRES_DB": "someapp",
|
"POSTGRES_DB": "someapp",
|
||||||
"POSTGRES_HOST": "someapp-postgres",
|
"POSTGRES_HOST": "postgres",
|
||||||
"DATABASE_URL": "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}",
|
"DATABASE_URL": "postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}/${POSTGRES_DB}"
|
||||||
"APP_ADMIN_USER": "admin@example.com",
|
|
||||||
"APP_ADMIN_PASSWORD": "someapp123456",
|
|
||||||
"SECRET_KEY_BASE": "some_key",
|
|
||||||
"SMTP_USERNAME": "admin@example.com",
|
|
||||||
"SMTP_PASSWORD": "",
|
|
||||||
"SMTP_ADDRESS": "someapp-smtp",
|
|
||||||
"SMTP_DOMAIN": "example.com",
|
|
||||||
"MAPS_API_KEY": "",
|
|
||||||
"TWILIO_ACCOUNT_SID": "",
|
|
||||||
"TWILIO_AUTH_TOKEN": "",
|
|
||||||
"TWILIO_SENDER_NUMBER": ""
|
|
||||||
},
|
},
|
||||||
"containers": {
|
"containers": {
|
||||||
"someapp": {
|
"someapp": {
|
||||||
@ -63,7 +52,6 @@
|
|||||||
],
|
],
|
||||||
"volumes": {
|
"volumes": {
|
||||||
"conf": "/srv/web2py/applications/app/models",
|
"conf": "/srv/web2py/applications/app/models",
|
||||||
"data-Spotter": "/srv/web2py/applications/app/modules/templates/Spotter",
|
|
||||||
"data-databases": "/srv/web2py/applications/app/databases",
|
"data-databases": "/srv/web2py/applications/app/databases",
|
||||||
"data-errors": "/srv/web2py/applications/app/errors",
|
"data-errors": "/srv/web2py/applications/app/errors",
|
||||||
"data-sessions": "/srv/web2py/applications/app/sessions",
|
"data-sessions": "/srv/web2py/applications/app/sessions",
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
[spoc]
|
|
||||||
data-dir = /some/data/dir
|
|
||||||
repo-url = https://user:pass@example.com/spoc/
|
|
@ -1,3 +0,0 @@
|
|||||||
RAILS_ENV=test
|
|
||||||
POSTGRES_PASSWORD=asdf=1234
|
|
||||||
SOMEKEY=someval
|
|
@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"RAILS_ENV": "test",
|
|
||||||
"POSTGRES_PASSWORD": "asdf=1234",
|
|
||||||
"SOMEKEY": "someval"
|
|
||||||
}
|
|
@ -65,7 +65,8 @@ def test_depsolver_complex():
|
|||||||
|
|
||||||
# Order within the same batch (i.e. items not depending on each other) can be random
|
# Order within the same batch (i.e. items not depending on each other) can be random
|
||||||
assert list(sorted(resolved[:2])) == ['dep3', 'dep9']
|
assert list(sorted(resolved[:2])) == ['dep3', 'dep9']
|
||||||
assert list(sorted(resolved[2:9])) == ['dep10', 'dep12', 'dep13', 'dep14', 'dep2', 'dep4', 'dep7']
|
assert list(sorted(resolved[2:9])) == ['dep10', 'dep12', 'dep13', 'dep14',
|
||||||
|
'dep2', 'dep4', 'dep7']
|
||||||
assert list(sorted(resolved[9:12])) == ['dep11', 'dep6', 'dep8']
|
assert list(sorted(resolved[9:12])) == ['dep11', 'dep6', 'dep8']
|
||||||
assert list(sorted(resolved[12:])) == ['dep1', 'dep5']
|
assert list(sorted(resolved[12:])) == ['dep1', 'dep5']
|
||||||
|
|
||||||
|
@ -1,29 +1,36 @@
|
|||||||
import errno
|
import errno
|
||||||
import fcntl
|
import fcntl
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import patch, call, mock_open
|
from unittest.mock import call, patch, mock_open
|
||||||
|
|
||||||
|
from spoc import config
|
||||||
from spoc import flock
|
from spoc import flock
|
||||||
|
|
||||||
def fail_callback(pid):
|
@flock.locked()
|
||||||
print(f'Lock held by {pid}')
|
|
||||||
|
|
||||||
@flock.locked('test.lock', fail_callback=fail_callback)
|
|
||||||
def mock_func():
|
def mock_func():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@patch('builtins.open', new_callable=mock_open, read_data='foo\0arg1\0arg2\n')
|
||||||
|
def test_print_lock(cmdline_open, capsys):
|
||||||
|
flock.print_lock('123')
|
||||||
|
|
||||||
|
cmdline_open.assert_called_once_with('/proc/123/cmdline')
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.err == 'Waiting for lock currently held by process 123 - foo arg1 arg2\n'
|
||||||
|
|
||||||
|
@patch('spoc.flock.print_lock')
|
||||||
@patch('fcntl.flock')
|
@patch('fcntl.flock')
|
||||||
@patch('time.sleep')
|
@patch('time.sleep')
|
||||||
@patch('os.getpid', return_value=1234)
|
@patch('os.getpid', return_value=1234)
|
||||||
@patch('builtins.open', new_callable=mock_open)
|
@patch('builtins.open', new_callable=mock_open)
|
||||||
def test_lock_success(lock_open, getpid, sleep, fcntl_flock):
|
def test_locked_success(lock_open, getpid, sleep, fcntl_flock, print_lock):
|
||||||
mock_func()
|
mock_func()
|
||||||
|
|
||||||
lock_open.assert_has_calls([
|
lock_open.assert_has_calls([
|
||||||
call('test.lock', 'a'),
|
call(config.LOCK_FILE, 'a'),
|
||||||
call().__enter__(),
|
call().__enter__(),
|
||||||
call().__exit__(None, None, None),
|
call().__exit__(None, None, None),
|
||||||
call('test.lock', 'r+'),
|
call(config.LOCK_FILE, 'r+'),
|
||||||
call().__enter__(),
|
call().__enter__(),
|
||||||
call().truncate(),
|
call().truncate(),
|
||||||
call().write('1234'),
|
call().write('1234'),
|
||||||
@ -34,12 +41,14 @@ def test_lock_success(lock_open, getpid, sleep, fcntl_flock):
|
|||||||
fcntl_flock.assert_called_once_with(lock_open(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl_flock.assert_called_once_with(lock_open(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
sleep.assert_not_called()
|
sleep.assert_not_called()
|
||||||
getpid.assert_called_once()
|
getpid.assert_called_once()
|
||||||
|
print_lock.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.flock.print_lock')
|
||||||
@patch('fcntl.flock')
|
@patch('fcntl.flock')
|
||||||
@patch('time.sleep')
|
@patch('time.sleep')
|
||||||
@patch('os.getpid', return_value=5678)
|
@patch('os.getpid', return_value=5678)
|
||||||
@patch('builtins.open', new_callable=mock_open, read_data='1234')
|
@patch('builtins.open', new_callable=mock_open, read_data='1234')
|
||||||
def test_lock_fail(lock_open, getpid, sleep, fcntl_flock, capsys):
|
def test_locked_fail(lock_open, getpid, sleep, fcntl_flock, print_lock):
|
||||||
fcntl_flock.side_effect = [
|
fcntl_flock.side_effect = [
|
||||||
OSError(errno.EAGAIN, 'in use'),
|
OSError(errno.EAGAIN, 'in use'),
|
||||||
OSError(errno.EAGAIN, 'in use'),
|
OSError(errno.EAGAIN, 'in use'),
|
||||||
@ -49,10 +58,10 @@ def test_lock_fail(lock_open, getpid, sleep, fcntl_flock, capsys):
|
|||||||
mock_func()
|
mock_func()
|
||||||
|
|
||||||
lock_open.assert_has_calls([
|
lock_open.assert_has_calls([
|
||||||
call('test.lock', 'a'),
|
call(config.LOCK_FILE, 'a'),
|
||||||
call().__enter__(),
|
call().__enter__(),
|
||||||
call().__exit__(None, None, None),
|
call().__exit__(None, None, None),
|
||||||
call('test.lock', 'r+'),
|
call(config.LOCK_FILE, 'r+'),
|
||||||
call().__enter__(),
|
call().__enter__(),
|
||||||
call().read(),
|
call().read(),
|
||||||
call().seek(0),
|
call().seek(0),
|
||||||
@ -67,30 +76,29 @@ def test_lock_fail(lock_open, getpid, sleep, fcntl_flock, capsys):
|
|||||||
expected_sleep_call = call(0.1)
|
expected_sleep_call = call(0.1)
|
||||||
assert sleep.call_args_list.count(expected_sleep_call) == 2
|
assert sleep.call_args_list.count(expected_sleep_call) == 2
|
||||||
getpid.assert_called_once()
|
getpid.assert_called_once()
|
||||||
|
print_lock.assert_called_once_with('1234')
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
@patch('spoc.flock.print_lock')
|
||||||
assert captured.out == 'Lock held by 1234\n'
|
|
||||||
|
|
||||||
@patch('fcntl.flock', side_effect=OSError(errno.EBADF, 'nope'))
|
@patch('fcntl.flock', side_effect=OSError(errno.EBADF, 'nope'))
|
||||||
@patch('time.sleep')
|
@patch('time.sleep')
|
||||||
@patch('os.getpid', return_value=5678)
|
@patch('os.getpid', return_value=5678)
|
||||||
@patch('builtins.open', new_callable=mock_open, read_data='1234')
|
@patch('builtins.open', new_callable=mock_open, read_data='1234')
|
||||||
def test_lock_error(lock_open, getpid, sleep, fcntl_flock):
|
def test_locked_error(lock_open, getpid, sleep, fcntl_flock, print_lock):
|
||||||
with pytest.raises(OSError):
|
with pytest.raises(OSError):
|
||||||
mock_func()
|
mock_func()
|
||||||
|
|
||||||
# Last call is
|
# Last call is
|
||||||
# call().__exit__(<class 'OSError'>, OSError(9, 'nope'), <traceback object at 0xaddress>)
|
# call().__exit__(<class 'OSError'>, OSError(9, 'nope'), <traceback object at 0xaddress>)
|
||||||
# The exception can be captured above and checked as follows
|
# The exception can be passed by the context manager above and checked as follows
|
||||||
# call().__exit__(ex.type, ex.value, ex.tb.tb_next.tb_next.tb_next)
|
# call().__exit__(ex.type, ex.value, ex.tb.tb_next.tb_next.tb_next)
|
||||||
# but it may by CPython specific, and frankly, that tb_next chain looks horrible.
|
# but it may by CPython specific, and frankly, that tb_next chain looks horrible.
|
||||||
# hence checking just the method and comparing the args with themselves
|
# hence checking just the method and comparing the args with themselves
|
||||||
last_exit_call_args = lock_open().__exit__.call_args_list[-1][0]
|
last_exit_call_args = lock_open().__exit__.call_args_list[-1][0]
|
||||||
lock_open.assert_has_calls([
|
lock_open.assert_has_calls([
|
||||||
call('test.lock', 'a'),
|
call(config.LOCK_FILE, 'a'),
|
||||||
call().__enter__(),
|
call().__enter__(),
|
||||||
call().__exit__(None, None, None),
|
call().__exit__(None, None, None),
|
||||||
call('test.lock', 'r+'),
|
call(config.LOCK_FILE, 'r+'),
|
||||||
call().__enter__(),
|
call().__enter__(),
|
||||||
call().__exit__(*last_exit_call_args),
|
call().__exit__(*last_exit_call_args),
|
||||||
])
|
])
|
||||||
@ -98,3 +106,4 @@ def test_lock_error(lock_open, getpid, sleep, fcntl_flock):
|
|||||||
fcntl_flock.assert_called_once_with(lock_open(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl_flock.assert_called_once_with(lock_open(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
sleep.assert_not_called()
|
sleep.assert_not_called()
|
||||||
getpid.assert_not_called()
|
getpid.assert_not_called()
|
||||||
|
print_lock.assert_not_called()
|
||||||
|
@ -7,92 +7,108 @@ from spoc import podman
|
|||||||
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
TEST_DATA_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('subprocess.run')
|
||||||
def test_get_apps(run):
|
def test_run(run):
|
||||||
|
process = podman.run(['foo', 'bar'])
|
||||||
|
|
||||||
|
run.assert_called_once_with(['foo', 'bar'], check=True, env=podman.ENV)
|
||||||
|
assert process == run.return_value
|
||||||
|
|
||||||
|
@patch('subprocess.run')
|
||||||
|
def test_out(run):
|
||||||
|
run.return_value.stdout = 'RESULT\n'
|
||||||
|
output = podman.out(['foo', 'bar'], arg1=123, arg2=True)
|
||||||
|
|
||||||
|
run.assert_called_once_with(['foo', 'bar'], stdout=subprocess.PIPE, text=True, check=True,
|
||||||
|
env=podman.ENV, arg1=123, arg2=True)
|
||||||
|
assert output == 'RESULT'
|
||||||
|
|
||||||
|
@patch('spoc.podman.out')
|
||||||
|
def test_get_apps(out):
|
||||||
with open(os.path.join(TEST_DATA_DIR, 'podman_pod_ps.json')) as f:
|
with open(os.path.join(TEST_DATA_DIR, 'podman_pod_ps.json')) as f:
|
||||||
run.return_value.stdout = f.read()
|
out.return_value = f.read()
|
||||||
|
|
||||||
pods = podman.get_apps()
|
pods = podman.get_apps()
|
||||||
|
|
||||||
expected_cmd = ['podman', 'pod', 'ps', '--format', 'json']
|
expected_cmd = ['podman', 'pod', 'ps', '--format', 'json']
|
||||||
run.assert_called_once_with(expected_cmd, check=True, stdout=subprocess.PIPE, text=True)
|
out.assert_called_once_with(expected_cmd)
|
||||||
assert pods == {'someapp': '0.1', 'anotherapp': '0.2', 'yetanotherapp': '0.3'}
|
assert pods == {'someapp': '0.1', 'anotherapp': '0.2', 'yetanotherapp': '0.3'}
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.out')
|
||||||
def test_get_volumes_for_app(run):
|
def test_get_volumes_for_app(out):
|
||||||
with open(os.path.join(TEST_DATA_DIR, 'podman_volume_ls.json')) as f:
|
with open(os.path.join(TEST_DATA_DIR, 'podman_volume_ls.json')) as f:
|
||||||
run.return_value.stdout = f.read()
|
out.return_value = f.read()
|
||||||
|
|
||||||
volumes = podman.get_volumes_for_app('someapp')
|
volumes = podman.get_volumes_for_app('someapp')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'volume', 'ls', '--filter', 'label=spoc.app=someapp',
|
expected_cmd = ['podman', 'volume', 'ls', '--filter', 'label=spoc.app=someapp',
|
||||||
'--format', 'json']
|
'--format', 'json']
|
||||||
run.asert_called_once_with(expected_cmd, check=True, stdout=subprocess.PIPE, text=True)
|
out.asert_called_once_with(expected_cmd)
|
||||||
assert volumes == {'someapp-conf', 'someapp-data'}
|
assert volumes == {'someapp-conf', 'someapp-data'}
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_start_pod(run):
|
def test_start_pod(run):
|
||||||
podman.start_pod('someapp')
|
podman.start_pod('someapp')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'pod', 'start', 'someapp']
|
expected_cmd = ['podman', 'pod', 'start', 'someapp']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_stop_pod(run):
|
def test_stop_pod(run):
|
||||||
podman.stop_pod('someapp')
|
podman.stop_pod('someapp')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'pod', 'stop', '--ignore', 'someapp']
|
expected_cmd = ['podman', 'pod', 'stop', '--ignore', 'someapp']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.out')
|
||||||
def test_get_pod_status(run):
|
def test_get_pod_status(out):
|
||||||
run.return_value.stdout = 'RESULT\n'
|
out.return_value = 'RESULT'
|
||||||
status = podman.get_pod_status('someapp')
|
status = podman.get_pod_status('someapp')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'pod', 'ps', '--filter', 'label=spoc.app=someapp']
|
expected_cmd = ['podman', 'pod', 'ps', '--filter', 'label=spoc.app=someapp']
|
||||||
run.assert_called_once_with(expected_cmd, check=True, stdout=subprocess.PIPE, text=True)
|
out.assert_called_once_with(expected_cmd)
|
||||||
assert status == 'RESULT'
|
assert status == 'RESULT'
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.out')
|
||||||
def test_get_pod_status_all(run):
|
def test_get_pod_status_all(out):
|
||||||
run.return_value.stdout = 'RESULT\n'
|
out.return_value = 'RESULT'
|
||||||
status = podman.get_pod_status()
|
status = podman.get_pod_status()
|
||||||
|
|
||||||
expected_cmd = ['podman', 'pod', 'ps']
|
expected_cmd = ['podman', 'pod', 'ps']
|
||||||
run.assert_called_once_with(expected_cmd, check=True, stdout=subprocess.PIPE, text=True)
|
out.assert_called_once_with(expected_cmd)
|
||||||
assert status == 'RESULT'
|
assert status == 'RESULT'
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_create_volume(run):
|
def test_create_volume(run):
|
||||||
podman.create_volume('someapp', 'someapp-vol')
|
podman.create_volume('someapp', 'someapp-vol')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'volume', 'create', '--label', 'spoc.app=someapp', 'someapp-vol']
|
expected_cmd = ['podman', 'volume', 'create', '--label', 'spoc.app=someapp', 'someapp-vol']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_remove_volume(run):
|
def test_remove_volume(run):
|
||||||
podman.remove_volume('someapp-vol')
|
podman.remove_volume('someapp-vol')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'volume', 'rm', 'someapp-vol']
|
expected_cmd = ['podman', 'volume', 'rm', 'someapp-vol']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_create_pod(run):
|
def test_create_pod(run):
|
||||||
podman.create_pod('someapp', '0.1')
|
podman.create_pod('someapp', '0.1')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'pod', 'create', '--name', 'someapp',
|
expected_cmd = ['podman', 'pod', 'create', '--name', 'someapp',
|
||||||
'--label', 'spoc.app=someapp', '--label', 'spoc.version=0.1']
|
'--label', 'spoc.app=someapp', '--label', 'spoc.version=0.1']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('spoc.podman.stop_pod')
|
@patch('spoc.podman.stop_pod')
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_remove_pod(run, stop_pod):
|
def test_remove_pod(run, stop_pod):
|
||||||
podman.remove_pod('someapp')
|
podman.remove_pod('someapp')
|
||||||
|
|
||||||
stop_pod.assert_called_once_with('someapp')
|
stop_pod.assert_called_once_with('someapp')
|
||||||
expected_cmd = ['podman', 'pod', 'rm', '--ignore', 'someapp']
|
expected_cmd = ['podman', 'pod', 'rm', '--ignore', 'someapp']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_create_container(run):
|
def test_create_container(run):
|
||||||
podman.create_container('someapp', 'someapp-cnt', 'example.com/someapp:0.23.6-210515',
|
podman.create_container('someapp', 'someapp-cnt', 'example.com/someapp:0.23.6-210515',
|
||||||
env_file='/var/lib/spoc/someapp.env',
|
env_file='/var/lib/spoc/someapp.env',
|
||||||
@ -106,21 +122,21 @@ def test_create_container(run):
|
|||||||
'--volume', 'someapp-srv:/srv', '--add-host', 'cnt:127.0.0.1',
|
'--volume', 'someapp-srv:/srv', '--add-host', 'cnt:127.0.0.1',
|
||||||
'--add-host', 'cnt2:127.0.0.1', '--add-host', 'cnt3:127.0.0.1',
|
'--add-host', 'cnt2:127.0.0.1', '--add-host', 'cnt3:127.0.0.1',
|
||||||
'example.com/someapp:0.23.6-210515']
|
'example.com/someapp:0.23.6-210515']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_create_container_minimal(run):
|
def test_create_container_minimal(run):
|
||||||
podman.create_container('someapp', 'someapp-cnt', 'example.com/someapp:0.23.6-210515')
|
podman.create_container('someapp', 'someapp-cnt', 'example.com/someapp:0.23.6-210515')
|
||||||
|
|
||||||
expected_cmd = ['podman', 'container', 'create', '--name', 'someapp-cnt', '--pod', 'someapp',
|
expected_cmd = ['podman', 'container', 'create', '--name', 'someapp-cnt', '--pod', 'someapp',
|
||||||
'--restart', 'unless-stopped', 'example.com/someapp:0.23.6-210515']
|
'--restart', 'unless-stopped', 'example.com/someapp:0.23.6-210515']
|
||||||
run.assert_called_once_with(expected_cmd, check=True)
|
run.assert_called_once_with(expected_cmd)
|
||||||
|
|
||||||
@patch('subprocess.run')
|
@patch('spoc.podman.run')
|
||||||
def test_prune(run):
|
def test_prune(run):
|
||||||
podman.prune()
|
podman.prune()
|
||||||
|
|
||||||
run.assert_has_calls([
|
run.assert_has_calls([
|
||||||
call(['podman', 'image', 'prune', '--all', '--force'], check=True),
|
call(['podman', 'image', 'prune', '--all', '--force']),
|
||||||
call(['podman', 'volume', 'prune', '--force'], check=True),
|
call(['podman', 'volume', 'prune', '--force']),
|
||||||
])
|
])
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import pytest
|
||||||
from unittest.mock import patch, call
|
from unittest.mock import patch, call
|
||||||
|
|
||||||
from spoc import config
|
from spoc import config
|
||||||
@ -8,7 +9,7 @@ from spoc import repo
|
|||||||
def test_load(req_get):
|
def test_load(req_get):
|
||||||
repo.load()
|
repo.load()
|
||||||
|
|
||||||
req_get.assert_called_once_with(config.REPO_FILE_URL, timeout=5)
|
req_get.assert_called_once_with(config.REPO_FILE_URL, auth=config.REGISTRY_AUTH, timeout=5)
|
||||||
req_get.return_value.raise_for_status.asert_called_once()
|
req_get.return_value.raise_for_status.asert_called_once()
|
||||||
req_get.return_value.json.asert_called_once()
|
req_get.return_value.json.asert_called_once()
|
||||||
|
|
||||||
@ -18,7 +19,7 @@ def test_load_twice_no_force(req_get):
|
|||||||
repo.load()
|
repo.load()
|
||||||
repo.load()
|
repo.load()
|
||||||
|
|
||||||
req_get.assert_called_once_with(config.REPO_FILE_URL, timeout=5)
|
req_get.assert_called_once_with(config.REPO_FILE_URL, auth=config.REGISTRY_AUTH, timeout=5)
|
||||||
req_get.return_value.raise_for_status.asert_called_once()
|
req_get.return_value.raise_for_status.asert_called_once()
|
||||||
req_get.return_value.json.asert_called_once()
|
req_get.return_value.json.asert_called_once()
|
||||||
|
|
||||||
@ -28,13 +29,24 @@ def test_load_twice_force(req_get):
|
|||||||
repo.load()
|
repo.load()
|
||||||
repo.load(force=True)
|
repo.load(force=True)
|
||||||
|
|
||||||
expected_call = call(config.REPO_FILE_URL, timeout=5)
|
expected_call = call(config.REPO_FILE_URL, auth=config.REGISTRY_AUTH, timeout=5)
|
||||||
assert req_get.call_args_list.count(expected_call) == 2
|
assert req_get.call_args_list.count(expected_call) == 2
|
||||||
assert req_get.return_value.raise_for_status.call_count == 2
|
assert req_get.return_value.raise_for_status.call_count == 2
|
||||||
assert req_get.return_value.json.call_count == 2
|
assert req_get.return_value.json.call_count == 2
|
||||||
|
|
||||||
|
@patch('spoc.repo._data', {'someapp': {'version': '0.1'}})
|
||||||
|
@patch('requests.get', side_effect=IOError())
|
||||||
|
def test_load_empty_on_fail(req_get):
|
||||||
|
with pytest.raises(IOError):
|
||||||
|
repo.load(force=True)
|
||||||
|
|
||||||
|
req_get.assert_called_once_with(config.REPO_FILE_URL, auth=config.REGISTRY_AUTH, timeout=5)
|
||||||
|
assert repo._data == {} # pylint: disable=protected-access
|
||||||
|
|
||||||
|
@patch('spoc.repo._data', {'someapp': {'version': '0.1'}})
|
||||||
@patch('spoc.repo.load')
|
@patch('spoc.repo.load')
|
||||||
def test_get_apps(repo_load):
|
def test_get_apps(repo_load):
|
||||||
repo.get_apps()
|
apps = repo.get_apps()
|
||||||
|
|
||||||
repo_load.assert_called_once()
|
repo_load.assert_called_once()
|
||||||
|
assert apps == {'someapp': {'version': '0.1'}}
|
||||||
|
@ -1,111 +1,220 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from argparse import Namespace
|
from unittest.mock import call, patch
|
||||||
from unittest.mock import call, mock_open, patch
|
|
||||||
|
|
||||||
import spoc
|
import spoc
|
||||||
|
|
||||||
@patch('builtins.open', new_callable=mock_open, read_data='foo\0arg1\0arg2\n')
|
def test_apperror():
|
||||||
def test_print_lock(cmdline_open, capsys):
|
exception = spoc.AppError('someapp')
|
||||||
spoc.print_lock('123')
|
|
||||||
|
|
||||||
cmdline_open.assert_called_once_with('/proc/123/cmdline')
|
assert exception.app_name == 'someapp'
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert captured.out == 'Waiting for lock currently held by process 123 - foo arg1 arg2\n'
|
|
||||||
|
|
||||||
@patch('spoc.podman.get_apps', return_value={'anotherapp': '0.1', 'someapp': '0.1'})
|
def test_appalreadyinstallederror():
|
||||||
def test_listing_installed(get_apps, capsys):
|
exception = spoc.AppAlreadyInstalledError('someapp')
|
||||||
spoc.listing('installed')
|
|
||||||
|
assert exception.app_name == 'someapp'
|
||||||
|
assert isinstance(exception, spoc.AppError)
|
||||||
|
|
||||||
|
def test_appnotinstallederror():
|
||||||
|
exception = spoc.AppNotInstalledError('someapp')
|
||||||
|
|
||||||
|
assert exception.app_name == 'someapp'
|
||||||
|
assert isinstance(exception, spoc.AppError)
|
||||||
|
|
||||||
|
def test_appnotinrepoerror():
|
||||||
|
exception = spoc.AppNotInRepoError('someapp')
|
||||||
|
|
||||||
|
assert exception.app_name == 'someapp'
|
||||||
|
assert isinstance(exception, spoc.AppError)
|
||||||
|
|
||||||
|
def test_appnotupdateableerror():
|
||||||
|
exception = spoc.AppNotUpdateableError('someapp')
|
||||||
|
|
||||||
|
assert exception.app_name == 'someapp'
|
||||||
|
assert isinstance(exception, spoc.AppError)
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.2', 'anotherapp': '0.1'})
|
||||||
|
def test_list_installed(get_apps):
|
||||||
|
apps = spoc.list_installed()
|
||||||
|
|
||||||
get_apps.assert_called_once()
|
get_apps.assert_called_once()
|
||||||
|
|
||||||
# Order is important here
|
assert apps == {'anotherapp': '0.1', 'someapp': '0.2'}
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert captured.out == 'anotherapp 0.1\nsomeapp 0.1\n'
|
|
||||||
|
|
||||||
@patch('spoc.repo.get_apps')
|
@patch('spoc.repo.get_apps',
|
||||||
def test_listing_online(get_apps):
|
return_value={'someapp': {'version': '0.2'}, 'anotherapp': {'version': '0.1'}})
|
||||||
spoc.listing('online')
|
def test_list_online(get_apps):
|
||||||
|
apps = spoc.list_online()
|
||||||
|
|
||||||
get_apps.assert_called_once()
|
get_apps.assert_called_once()
|
||||||
|
|
||||||
|
assert apps == {'anotherapp': '0.1', 'someapp': '0.2'}
|
||||||
|
|
||||||
@patch('spoc.repo.get_apps',
|
@patch('spoc.repo.get_apps',
|
||||||
return_value={'someapp': {'version': '0.2'}, 'anotherapp': {'version': '0.1'}})
|
return_value={'someapp': {'version': '0.2'}, 'anotherapp': {'version': '0.1'}})
|
||||||
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
def test_listing_updates(repo_get_apps, podman_get_apps, capsys):
|
def test_list_updates(podman_get_apps, repo_get_apps):
|
||||||
spoc.listing('updates')
|
apps = spoc.list_updates()
|
||||||
|
|
||||||
repo_get_apps.assert_called_once()
|
repo_get_apps.assert_called_once()
|
||||||
podman_get_apps.assert_called_once()
|
podman_get_apps.assert_called_once()
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
assert apps == {'someapp': '0.1 -> 0.2'}
|
||||||
assert captured.out == 'someapp 0.1 -> 0.2\n'
|
|
||||||
|
|
||||||
@patch('spoc.repo.get_apps')
|
|
||||||
@patch('spoc.podman.get_apps')
|
|
||||||
def test_listing_invalid(repo_get_apps, podman_get_apps, capsys):
|
|
||||||
spoc.listing('invalid')
|
|
||||||
|
|
||||||
repo_get_apps.assert_not_called()
|
|
||||||
podman_get_apps.assert_not_called()
|
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
|
||||||
assert captured.out == ''
|
|
||||||
|
|
||||||
|
@patch('spoc.repo.get_apps', return_value={'someapp': {'version': '0.1'}})
|
||||||
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
@patch('spoc.app.install')
|
@patch('spoc.app.install')
|
||||||
def test_install(app_install):
|
def test_install(app_install, podman_get_apps, repo_get_apps):
|
||||||
spoc.install('someapp')
|
spoc.install.__wrapped__('someapp')
|
||||||
|
#spoc.install('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
repo_get_apps.assert_called_once()
|
||||||
app_install.assert_called_once_with('someapp')
|
app_install.assert_called_once_with('someapp')
|
||||||
|
|
||||||
@patch('spoc.app.update')
|
@patch('spoc.repo.get_apps', return_value={'someapp': {'version': '0.1'}})
|
||||||
def test_update(app_update):
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
spoc.update('someapp')
|
@patch('spoc.app.install')
|
||||||
|
def test_install_already_installed(app_install, podman_get_apps, repo_get_apps):
|
||||||
|
with pytest.raises(spoc.AppAlreadyInstalledError):
|
||||||
|
spoc.install.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
repo_get_apps.assert_not_called()
|
||||||
|
app_install.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.repo.get_apps', return_value={})
|
||||||
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
|
@patch('spoc.app.install')
|
||||||
|
def test_install_not_in_repo(app_install, podman_get_apps, repo_get_apps):
|
||||||
|
with pytest.raises(spoc.AppNotInRepoError):
|
||||||
|
spoc.install.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
repo_get_apps.assert_called_once()
|
||||||
|
app_install.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.list_updates', return_value={'someapp': '0.1 -> 0.2'})
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
|
@patch('spoc.app.update')
|
||||||
|
def test_update(app_update, podman_get_apps, list_updates):
|
||||||
|
spoc.update.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
list_updates.assert_called_once()
|
||||||
app_update.assert_called_once_with('someapp')
|
app_update.assert_called_once_with('someapp')
|
||||||
|
|
||||||
@patch('spoc.app.uninstall')
|
@patch('spoc.list_updates', return_value={})
|
||||||
def test_uninstall(app_uninstall):
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
spoc.uninstall('someapp')
|
@patch('spoc.app.update')
|
||||||
|
def test_update_not_installed(app_update, podman_get_apps, list_updates):
|
||||||
|
with pytest.raises(spoc.AppNotInstalledError):
|
||||||
|
spoc.update.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
list_updates.assert_not_called()
|
||||||
|
app_update.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.list_updates', return_value={})
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
|
@patch('spoc.app.update')
|
||||||
|
def test_update_not_updateable(app_update, podman_get_apps, list_updates):
|
||||||
|
with pytest.raises(spoc.AppNotUpdateableError):
|
||||||
|
spoc.update.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
list_updates.assert_called_once()
|
||||||
|
app_update.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
|
@patch('spoc.app.uninstall')
|
||||||
|
def test_uninstall(app_uninstall, podman_get_apps):
|
||||||
|
spoc.uninstall.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
app_uninstall.assert_called_once_with('someapp')
|
app_uninstall.assert_called_once_with('someapp')
|
||||||
|
|
||||||
@patch('spoc.podman.start_pod')
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
def test_start(start_pod):
|
@patch('spoc.app.uninstall')
|
||||||
spoc.start('someapp')
|
def test_uninstall_not_installed(app_uninstall, podman_get_apps):
|
||||||
|
with pytest.raises(spoc.AppNotInstalledError):
|
||||||
|
spoc.uninstall.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
app_uninstall.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
|
@patch('spoc.podman.start_pod')
|
||||||
|
def test_start(start_pod, podman_get_apps):
|
||||||
|
spoc.start.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
start_pod.assert_called_once_with('someapp')
|
start_pod.assert_called_once_with('someapp')
|
||||||
|
|
||||||
@patch('spoc.podman.stop_pod')
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
def test_stop(stop_pod):
|
@patch('spoc.podman.start_pod')
|
||||||
spoc.stop('someapp')
|
def test_start_not_installed(start_pod, podman_get_apps):
|
||||||
|
with pytest.raises(spoc.AppNotInstalledError):
|
||||||
|
spoc.start.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
start_pod.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
|
@patch('spoc.podman.stop_pod')
|
||||||
|
def test_stop(stop_pod, podman_get_apps):
|
||||||
|
spoc.stop.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
stop_pod.assert_called_once_with('someapp')
|
stop_pod.assert_called_once_with('someapp')
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
|
@patch('spoc.podman.stop_pod')
|
||||||
|
def test_stop_not_installed(stop_pod, podman_get_apps):
|
||||||
|
with pytest.raises(spoc.AppNotInstalledError):
|
||||||
|
spoc.stop.__wrapped__('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
stop_pod.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
@patch('spoc.podman.get_pod_status', return_value='RESULT')
|
@patch('spoc.podman.get_pod_status', return_value='RESULT')
|
||||||
def test_status(get_pod_status, capsys):
|
def test_status(get_pod_status, podman_get_apps):
|
||||||
spoc.status('someapp')
|
status = spoc.status('someapp')
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
get_pod_status.assert_called_once_with('someapp')
|
get_pod_status.assert_called_once_with('someapp')
|
||||||
captured = capsys.readouterr()
|
assert status == 'RESULT'
|
||||||
assert captured.out == 'RESULT\n'
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('value,expected',[
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
('1', True),
|
@patch('spoc.podman.get_pod_status')
|
||||||
('on', True),
|
def test_status_not_installed(get_pod_status, podman_get_apps):
|
||||||
('Enable', True),
|
with pytest.raises(spoc.AppNotInstalledError):
|
||||||
('TRUE', True),
|
spoc.status('someapp')
|
||||||
('whatever', False),
|
|
||||||
])
|
podman_get_apps.assert_called_once()
|
||||||
|
get_pod_status.assert_not_called()
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1'})
|
||||||
@patch('spoc.autostart.set_app')
|
@patch('spoc.autostart.set_app')
|
||||||
def test_set_autostart(set_app, value, expected):
|
def test_set_autostart(set_app, podman_get_apps):
|
||||||
spoc.set_autostart('someapp', value)
|
spoc.set_autostart.__wrapped__('someapp', True)
|
||||||
|
|
||||||
set_app.assert_called_once_with('someapp', expected)
|
podman_get_apps.assert_called_once()
|
||||||
|
set_app.assert_called_once_with('someapp', True)
|
||||||
|
|
||||||
|
@patch('spoc.podman.get_apps', return_value={})
|
||||||
|
@patch('spoc.autostart.set_app')
|
||||||
|
def test_set_autostart_not_installed(set_app, podman_get_apps):
|
||||||
|
with pytest.raises(spoc.AppNotInstalledError):
|
||||||
|
spoc.set_autostart.__wrapped__('someapp', True)
|
||||||
|
|
||||||
|
podman_get_apps.assert_called_once()
|
||||||
|
set_app.assert_not_called()
|
||||||
|
|
||||||
@patch('spoc.autostart.get_apps', return_value={'someapp', 'anotherapp'})
|
@patch('spoc.autostart.get_apps', return_value={'someapp', 'anotherapp'})
|
||||||
@patch('spoc.podman.start_pod')
|
@patch('spoc.podman.start_pod')
|
||||||
def test_start_autostarted(start_pod, get_apps):
|
def test_start_autostarted(start_pod, get_apps):
|
||||||
spoc.start_autostarted()
|
spoc.start_autostarted.__wrapped__()
|
||||||
|
|
||||||
get_apps.assert_called_once()
|
get_apps.assert_called_once()
|
||||||
start_pod.assert_has_calls([
|
start_pod.assert_has_calls([
|
||||||
@ -116,7 +225,7 @@ def test_start_autostarted(start_pod, get_apps):
|
|||||||
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1', 'anotherapp': '0.1'})
|
@patch('spoc.podman.get_apps', return_value={'someapp': '0.1', 'anotherapp': '0.1'})
|
||||||
@patch('spoc.podman.stop_pod')
|
@patch('spoc.podman.stop_pod')
|
||||||
def test_stop_all(stop_pod, get_apps):
|
def test_stop_all(stop_pod, get_apps):
|
||||||
spoc.stop_all()
|
spoc.stop_all.__wrapped__()
|
||||||
|
|
||||||
get_apps.assert_called_once()
|
get_apps.assert_called_once()
|
||||||
stop_pod.assert_has_calls([
|
stop_pod.assert_has_calls([
|
||||||
@ -124,143 +233,16 @@ def test_stop_all(stop_pod, get_apps):
|
|||||||
call('anotherapp'),
|
call('anotherapp'),
|
||||||
], any_order=True)
|
], any_order=True)
|
||||||
|
|
||||||
|
@patch('spoc.config.write_auth')
|
||||||
|
@patch('spoc.repo.load')
|
||||||
|
def test_login(repo_load, write_auth):
|
||||||
|
spoc.login.__wrapped__('somehost', 'someuser', 'somepass')
|
||||||
|
|
||||||
|
write_auth.assert_called_once_with('somehost', 'someuser', 'somepass')
|
||||||
|
repo_load.assert_called_once_with(force=True)
|
||||||
|
|
||||||
@patch('spoc.podman.prune')
|
@patch('spoc.podman.prune')
|
||||||
def test_prune(prune):
|
def test_prune(prune):
|
||||||
spoc.prune()
|
spoc.prune.__wrapped__()
|
||||||
|
|
||||||
prune.assert_called_once()
|
prune.assert_called_once()
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'list'])
|
|
||||||
@patch('spoc.listing')
|
|
||||||
def test_main_listing(listing):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
listing.assert_called_once_with('installed')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'list', 'online'])
|
|
||||||
@patch('spoc.listing')
|
|
||||||
def test_main_listing_online(listing):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
listing.assert_called_once_with('online')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'install', 'someapp'])
|
|
||||||
@patch('spoc.install')
|
|
||||||
def test_main_install(install):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
install.assert_called_once_with('someapp')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'update', 'someapp'])
|
|
||||||
@patch('spoc.update')
|
|
||||||
def test_main_update(update):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
update.assert_called_once_with('someapp')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'uninstall', 'someapp'])
|
|
||||||
@patch('spoc.uninstall')
|
|
||||||
def test_main_uninstall(uninstall):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
uninstall.assert_called_once_with('someapp')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'start', 'someapp'])
|
|
||||||
@patch('spoc.start')
|
|
||||||
def test_main_start(start):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
start.assert_called_once_with('someapp')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'stop', 'someapp'])
|
|
||||||
@patch('spoc.stop')
|
|
||||||
def test_main_stop(stop):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
stop.assert_called_once_with('someapp')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'status', 'someapp'])
|
|
||||||
@patch('spoc.status')
|
|
||||||
def test_main_status(status):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
status.assert_called_once_with('someapp')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'status'])
|
|
||||||
@patch('spoc.status')
|
|
||||||
def test_main_status_all(status):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
status.assert_called_once_with(None)
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'autostart', 'someapp', 'on'])
|
|
||||||
@patch('spoc.set_autostart')
|
|
||||||
def test_main_autostart(autostart):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
autostart.assert_called_once_with('someapp', 'on')
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'start-autostarted'])
|
|
||||||
@patch('spoc.start_autostarted')
|
|
||||||
def test_main_start_autostarted(start_autostarted):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
start_autostarted.assert_called_once()
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'stop-all'])
|
|
||||||
@patch('spoc.stop_all')
|
|
||||||
def test_main_stop_all(stop_all):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
stop_all.assert_called_once()
|
|
||||||
|
|
||||||
@patch('sys.argv', ['foo', 'prune'])
|
|
||||||
@patch('spoc.prune')
|
|
||||||
def test_main_prune(prune):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
prune.assert_called_once()
|
|
||||||
|
|
||||||
@patch('spoc.parse_args', return_value=Namespace(action=None))
|
|
||||||
@patch('spoc.listing')
|
|
||||||
@patch('spoc.install')
|
|
||||||
@patch('spoc.update')
|
|
||||||
@patch('spoc.uninstall')
|
|
||||||
@patch('spoc.start')
|
|
||||||
@patch('spoc.stop')
|
|
||||||
@patch('spoc.status')
|
|
||||||
@patch('spoc.start_autostarted')
|
|
||||||
@patch('spoc.stop_all')
|
|
||||||
@patch('spoc.prune')
|
|
||||||
def test_main_invalid(prune, stop_all, start_autostarted, status, stop, start,
|
|
||||||
uninstall, update, install, listing, parse_args):
|
|
||||||
spoc.main()
|
|
||||||
|
|
||||||
parse_args.assert_called_once()
|
|
||||||
listing.assert_not_called()
|
|
||||||
install.assert_not_called()
|
|
||||||
update.assert_not_called()
|
|
||||||
uninstall.assert_not_called()
|
|
||||||
start.assert_not_called()
|
|
||||||
stop.assert_not_called()
|
|
||||||
status.assert_not_called()
|
|
||||||
start_autostarted.assert_not_called()
|
|
||||||
stop_all.assert_not_called()
|
|
||||||
prune.assert_not_called()
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('argv', [
|
|
||||||
['list', 'invalid'],
|
|
||||||
['install'],
|
|
||||||
['update'],
|
|
||||||
['uninstall'],
|
|
||||||
['start'],
|
|
||||||
['stop'],
|
|
||||||
['autostart'],
|
|
||||||
['autostart', 'someapp'],
|
|
||||||
['invalid'],
|
|
||||||
])
|
|
||||||
def test_main_systemexit(argv):
|
|
||||||
argv.insert(0, 'foo')
|
|
||||||
with patch('sys.argv', argv):
|
|
||||||
with pytest.raises(SystemExit):
|
|
||||||
spoc.main()
|
|
||||||
|
Loading…
Reference in New Issue
Block a user