1 Commits

Author SHA1 Message Date
dependabot[bot]
2bcac8ba51 Bump pytest from 4.3.1 to 9.0.3 in /src/tests
Bumps [pytest](https://github.com/pytest-dev/pytest) from 4.3.1 to 9.0.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/4.3.1...9.0.3)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 9.0.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 17:00:55 +00:00
8 changed files with 70 additions and 112 deletions

View File

@@ -4,7 +4,7 @@ sudo: false
matrix: matrix:
include: include:
python: 3.8 python: 3.7
dist: bionic dist: bionic
sudo: true sudo: true

View File

@@ -108,10 +108,10 @@ def status(config, containers):
logger.info( logger.info(
' - %s (is_ready=%s) -> %s', ' - %s (is_ready=%s) -> %s',
instance.container_type, instance.container_type,
ping, ping == 0,
instance.backup_destination_path(), instance.backup_destination_path(),
) )
if not ping: if ping != 0:
logger.error("Database '%s' in service %s cannot be reached", logger.error("Database '%s' in service %s cannot be reached",
instance.container_type, container.service_name) instance.container_type, container.service_name)

View File

@@ -1,9 +1,7 @@
import logging import logging
from typing import List, Tuple, Union from typing import List, Tuple
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from restic_compose_backup import utils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -11,33 +9,37 @@ def test():
return run(['ls', '/volumes']) return run(['ls', '/volumes'])
def ping_mysql(container_id, host, port, username, password) -> int: def ping_mysql(host, port, username) -> int:
"""Check if the mysql is up and can be reached""" """Check if the mysql is up and can be reached"""
return docker_exec(container_id, [ return run([
'mysqladmin', 'mysqladmin',
'ping', 'ping',
'--host',
host,
'--port',
port,
'--user', '--user',
username, username,
], environment={ ])
'MYSQL_PWD': password
})
def ping_mariadb(container_id, host, port, username, password) -> int: def ping_mariadb(host, port, username) -> int:
"""Check if the mariadb is up and can be reached""" """Check if the mariadb is up and can be reached"""
return docker_exec(container_id, [ return run([
'mysqladmin', 'mysqladmin',
'ping', 'ping',
'--host',
host,
'--port',
port,
'--user', '--user',
username, username,
], environment={ ])
'MYSQL_PWD': password
})
def ping_postgres(container_id, host, port, username, password) -> int: def ping_postgres(host, port, username, password) -> int:
"""Check if postgres can be reached""" """Check if postgres can be reached"""
return docker_exec(container_id, [ return run([
"pg_isready", "pg_isready",
f"--host={host}", f"--host={host}",
f"--port={port}", f"--port={port}",
@@ -45,22 +47,6 @@ def ping_postgres(container_id, host, port, username, password) -> int:
]) ])
def docker_exec(container_id: str, cmd: List[str], environment: Union[dict, list] = []) -> int:
"""Execute a command within the given container"""
client = utils.docker_client()
logger.debug('docker exec inside %s: %s', container_id, ' '.join(cmd))
exit_code, (stdout, stderr) = client.containers.get(container_id).exec_run(cmd, demux=True, environment=environment)
if stdout:
log_std('stdout', stdout.decode(),
logging.DEBUG if exit_code == 0 else logging.ERROR)
if stderr:
log_std('stderr', stderr.decode(), logging.ERROR)
return exit_code
def run(cmd: List[str]) -> int: def run(cmd: List[str]) -> int:
"""Run a command with parameters""" """Run a command with parameters"""
logger.debug('cmd: %s', ' '.join(cmd)) logger.debug('cmd: %s', ' '.join(cmd))

View File

@@ -25,19 +25,20 @@ class MariadbContainer(Container):
"""Check the availability of the service""" """Check the availability of the service"""
creds = self.get_credentials() creds = self.get_credentials()
return commands.ping_mariadb( with utils.environment('MYSQL_PWD', creds['password']):
self.id, return commands.ping_mariadb(
creds['host'], creds['host'],
creds['port'], creds['port'],
creds['username'], creds['username'],
creds['password'] )
) == 0
def dump_command(self) -> list: def dump_command(self) -> list:
"""list: create a dump command restic and use to send data through stdin""" """list: create a dump command restic and use to send data through stdin"""
creds = self.get_credentials() creds = self.get_credentials()
return [ return [
"mysqldump", "mysqldump",
f"--host={creds['host']}",
f"--port={creds['port']}",
f"--user={creds['username']}", f"--user={creds['username']}",
"--all-databases", "--all-databases",
"--no-tablespaces", "--no-tablespaces",
@@ -47,15 +48,12 @@ class MariadbContainer(Container):
config = Config() config = Config()
creds = self.get_credentials() creds = self.get_credentials()
return restic.backup_from_stdin( with utils.environment('MYSQL_PWD', creds['password']):
config.repository, return restic.backup_from_stdin(
self.backup_destination_path(), config.repository,
self.id, self.backup_destination_path(),
self.dump_command(), self.dump_command(),
environment={ )
'MYSQL_PWD': creds['password']
}
)
def backup_destination_path(self) -> str: def backup_destination_path(self) -> str:
destination = Path("/databases") destination = Path("/databases")
@@ -87,19 +85,20 @@ class MysqlContainer(Container):
"""Check the availability of the service""" """Check the availability of the service"""
creds = self.get_credentials() creds = self.get_credentials()
return commands.ping_mysql( with utils.environment('MYSQL_PWD', creds['password']):
self.id, return commands.ping_mysql(
creds['host'], creds['host'],
creds['port'], creds['port'],
creds['username'], creds['username'],
creds['password'] )
) == 0
def dump_command(self) -> list: def dump_command(self) -> list:
"""list: create a dump command restic and use to send data through stdin""" """list: create a dump command restic and use to send data through stdin"""
creds = self.get_credentials() creds = self.get_credentials()
return [ return [
"mysqldump", "mysqldump",
f"--host={creds['host']}",
f"--port={creds['port']}",
f"--user={creds['username']}", f"--user={creds['username']}",
"--all-databases", "--all-databases",
"--no-tablespaces", "--no-tablespaces",
@@ -109,15 +108,12 @@ class MysqlContainer(Container):
config = Config() config = Config()
creds = self.get_credentials() creds = self.get_credentials()
return restic.backup_from_stdin( with utils.environment('MYSQL_PWD', creds['password']):
config.repository, return restic.backup_from_stdin(
self.backup_destination_path(), config.repository,
self.id, self.backup_destination_path(),
self.dump_command(), self.dump_command(),
environment={ )
"MYSQL_PWD": creds['password']
}
)
def backup_destination_path(self) -> str: def backup_destination_path(self) -> str:
destination = Path("/databases") destination = Path("/databases")
@@ -150,12 +146,11 @@ class PostgresContainer(Container):
"""Check the availability of the service""" """Check the availability of the service"""
creds = self.get_credentials() creds = self.get_credentials()
return commands.ping_postgres( return commands.ping_postgres(
self.id,
creds['host'], creds['host'],
creds['port'], creds['port'],
creds['username'], creds['username'],
creds['password'], creds['password'],
) == 0 )
def dump_command(self) -> list: def dump_command(self) -> list:
"""list: create a dump command restic and use to send data through stdin""" """list: create a dump command restic and use to send data through stdin"""
@@ -163,19 +158,22 @@ class PostgresContainer(Container):
creds = self.get_credentials() creds = self.get_credentials()
return [ return [
"pg_dump", "pg_dump",
f"--host={creds['host']}",
f"--port={creds['port']}",
f"--username={creds['username']}", f"--username={creds['username']}",
creds['database'], creds['database'],
] ]
def backup(self): def backup(self):
config = Config() config = Config()
creds = self.get_credentials()
return restic.backup_from_stdin( with utils.environment('PGPASSWORD', creds['password']):
config.repository, return restic.backup_from_stdin(
self.backup_destination_path(), config.repository,
self.id, self.backup_destination_path(),
self.dump_command(), self.dump_command(),
) )
def backup_destination_path(self) -> str: def backup_destination_path(self) -> str:
destination = Path("/databases") destination = Path("/databases")

View File

@@ -2,10 +2,9 @@
Restic commands Restic commands
""" """
import logging import logging
from typing import List, Tuple, Union from typing import List, Tuple
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from restic_compose_backup import commands from restic_compose_backup import commands
from restic_compose_backup import utils
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,10 +27,9 @@ def backup_files(repository: str, source='/volumes'):
])) ]))
def backup_from_stdin(repository: str, filename: str, container_id: str, def backup_from_stdin(repository: str, filename: str, source_command: List[str]):
source_command: List[str], environment: Union[dict, list] = None):
""" """
Backs up from stdin running the source_command passed in within the given container. Backs up from stdin running the source_command passed in.
It will appear in restic with the filename (including path) passed in. It will appear in restic with the filename (including path) passed in.
""" """
dest_command = restic(repository, [ dest_command = restic(repository, [
@@ -41,43 +39,20 @@ def backup_from_stdin(repository: str, filename: str, container_id: str,
filename, filename,
]) ])
client = utils.docker_client() # pipe source command into dest command
source_process = Popen(source_command, stdout=PIPE, bufsize=65536)
logger.debug('docker exec inside %s: %s', container_id, ' '.join(source_command)) dest_process = Popen(dest_command, stdin=source_process.stdout, stdout=PIPE, stderr=PIPE, bufsize=65536)
# Create and start source command inside the given container
handle = client.api.exec_create(container_id, source_command, environment=environment)
exec_id = handle["Id"]
stream = client.api.exec_start(exec_id, stream=True, demux=True)
source_stderr = ""
# Create the restic process to receive the output of the source command
dest_process = Popen(dest_command, stdin=PIPE, stdout=PIPE, stderr=PIPE, bufsize=65536)
# Send the ouptut of the source command over to restic in the chunks received
for stdout_chunk, stderr_chunk in stream:
if stdout_chunk:
dest_process.stdin.write(stdout_chunk)
if stderr_chunk:
source_stderr += stderr_chunk.decode()
# Wait for restic to finish
stdout, stderr = dest_process.communicate() stdout, stderr = dest_process.communicate()
# Ensure both processes exited with code 0 # Ensure both processes exited with code 0
source_exit = client.api.exec_inspect(exec_id).get("ExitCode") source_exit, dest_exit = source_process.poll(), dest_process.poll()
dest_exit = dest_process.poll() exit_code = 0 if (source_exit == 0 and dest_exit == 0) else 1
exit_code = source_exit or dest_exit
if stdout: if stdout:
commands.log_std('stdout', stdout, logging.DEBUG if exit_code == 0 else logging.ERROR) commands.log_std('stdout', stdout, logging.DEBUG if exit_code == 0 else logging.ERROR)
if source_stderr:
commands.log_std(f'stderr ({source_command[0]})', source_stderr, logging.ERROR)
if stderr: if stderr:
commands.log_std('stderr (restic)', stderr, logging.ERROR) commands.log_std('stderr', stderr, logging.ERROR)
return exit_code return exit_code

View File

@@ -3,7 +3,6 @@ import logging
from typing import List, TYPE_CHECKING from typing import List, TYPE_CHECKING
from contextlib import contextmanager from contextlib import contextmanager
import docker import docker
from docker import DockerClient
if TYPE_CHECKING: if TYPE_CHECKING:
from restic_compose_backup.containers import Container from restic_compose_backup.containers import Container
@@ -13,7 +12,7 @@ logger = logging.getLogger(__name__)
TRUE_VALUES = ['1', 'true', 'True', True, 1] TRUE_VALUES = ['1', 'true', 'True', True, 1]
def docker_client() -> DockerClient: def docker_client():
""" """
Create a docker client from the following environment variables:: Create a docker client from the following environment variables::

View File

@@ -1,2 +1,2 @@
pytest==4.3.1 pytest==9.0.3
tox tox

View File

@@ -53,4 +53,4 @@ norecursedirs = tests/* .venv/* .tox/* build/ docs/
ignore = H405,D100,D101,D102,D103,D104,D105,D200,D202,D203,D204,D205,D211,D301,D400,D401,W503 ignore = H405,D100,D101,D102,D103,D104,D105,D200,D202,D203,D204,D205,D211,D301,D400,D401,W503
show-source = True show-source = True
max-line-length = 120 max-line-length = 120
exclude = .tox,env,tests,build,conf.py,.venv exclude = .tox,env,tests,build,conf.py