Skip to content

Commit 3a626c8

Browse files
committed
Improve stability: expand smoke tests and harden optional deps
1 parent 98d7648 commit 3a626c8

5 files changed

Lines changed: 282 additions & 153 deletions

File tree

.github/workflows/integration-tests.yml

Lines changed: 5 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -28,130 +28,8 @@ jobs:
2828
- name: Run Integration Matrix
2929
shell: bash
3030
run: |
31-
set -euo pipefail
32-
33-
IMAGE="a75g/docker-db-backup:ci"
34-
NET="dbbackup-ci-net"
35-
TMP_ROOT="${RUNNER_TEMP}/dbbackup-ci"
36-
PG_BACKUP_DIR="${TMP_ROOT}/pg"
37-
MYSQL_BACKUP_DIR="${TMP_ROOT}/mysql"
38-
REDIS_BACKUP_DIR="${TMP_ROOT}/redis"
39-
40-
mkdir -p "${PG_BACKUP_DIR}" "${MYSQL_BACKUP_DIR}" "${REDIS_BACKUP_DIR}"
41-
42-
cleanup() {
43-
docker rm -f dbbackup-pg pg18-ci dbbackup-mysql mysql8-ci dbbackup-redis redis7-ci >/dev/null 2>&1 || true
44-
docker network rm "${NET}" >/dev/null 2>&1 || true
45-
}
46-
trap cleanup EXIT
47-
cleanup
48-
docker network create "${NET}" >/dev/null
49-
50-
echo "=== PostgreSQL 18 backup + restore smoke ==="
51-
docker run -d --name pg18-ci --network "${NET}" \
52-
-e POSTGRES_PASSWORD=postgres \
53-
-e POSTGRES_DB=testdb \
54-
postgres:18 >/dev/null
55-
56-
for i in $(seq 1 60); do
57-
if docker exec pg18-ci pg_isready -U postgres >/dev/null 2>&1; then
58-
break
59-
fi
60-
sleep 2
61-
done
62-
63-
docker exec pg18-ci psql -U postgres -d testdb -c "CREATE TABLE IF NOT EXISTS ci_items(id int primary key, val text);" >/dev/null
64-
docker exec pg18-ci psql -U postgres -d testdb -c "TRUNCATE ci_items;" >/dev/null
65-
docker exec pg18-ci psql -U postgres -d testdb -c "INSERT INTO ci_items VALUES (1,'one'),(2,'two');" >/dev/null
66-
67-
docker run -d --name dbbackup-pg --network "${NET}" \
68-
-v "${PG_BACKUP_DIR}:/backup" \
69-
-e MODE=MANUAL \
70-
-e CONTAINER_ENABLE_SCHEDULING=FALSE \
71-
-e CONTAINER_ENABLE_MONITORING=FALSE \
72-
-e MANUAL_RUN_FOREVER=TRUE \
73-
-e DB01_TYPE=pgsql \
74-
-e DB01_NAME=testdb \
75-
-e DB01_HOST=pg18-ci \
76-
-e DB01_USER=postgres \
77-
-e DB01_PASS=postgres \
78-
-e DB01_PORT=5432 \
79-
-e DB01_BACKUP_LOCATION=FILESYSTEM \
80-
"${IMAGE}" >/dev/null
81-
82-
sleep 10
83-
docker exec dbbackup-pg backup-now >/dev/null
84-
85-
PG_FILE="$(docker exec dbbackup-pg sh -lc 'ls -1 /backup/pgsql_testdb_pg18-ci_*.sql.zst | head -n1 | xargs -n1 basename')"
86-
test -n "${PG_FILE}"
87-
88-
docker exec pg18-ci psql -U postgres -d postgres -c "DROP DATABASE IF EXISTS restoredb;" >/dev/null
89-
docker exec pg18-ci psql -U postgres -d postgres -c "CREATE DATABASE restoredb;" >/dev/null
90-
docker exec dbbackup-pg restore "/backup/${PG_FILE}" pgsql pg18-ci restoredb postgres postgres 5432 >/dev/null
91-
PG_COUNT="$(docker exec pg18-ci psql -U postgres -d restoredb -tAc "SELECT COUNT(*) FROM ci_items;")"
92-
test "${PG_COUNT}" = "2"
93-
94-
echo "=== MySQL 8 backup + restore smoke ==="
95-
docker run -d --name mysql8-ci --network "${NET}" \
96-
-e MYSQL_ROOT_PASSWORD=rootpass \
97-
-e MYSQL_DATABASE=appdb \
98-
mysql:8 >/dev/null
99-
100-
for i in $(seq 1 90); do
101-
if docker exec mysql8-ci sh -lc 'mysqladmin ping -h 127.0.0.1 -uroot -prootpass --silent >/dev/null 2>&1'; then
102-
break
103-
fi
104-
sleep 2
105-
done
106-
107-
docker exec mysql8-ci mysql -uroot -prootpass -e "CREATE DATABASE IF NOT EXISTS appdb; USE appdb; CREATE TABLE IF NOT EXISTS ci_items(id INT PRIMARY KEY, val VARCHAR(20)); TRUNCATE ci_items; INSERT INTO ci_items VALUES (1,'one'),(2,'two');" >/dev/null
108-
109-
docker run -d --name dbbackup-mysql --network "${NET}" \
110-
-v "${MYSQL_BACKUP_DIR}:/backup" \
111-
-e MODE=MANUAL \
112-
-e CONTAINER_ENABLE_SCHEDULING=FALSE \
113-
-e CONTAINER_ENABLE_MONITORING=FALSE \
114-
-e MANUAL_RUN_FOREVER=TRUE \
115-
-e DB01_TYPE=mysql \
116-
-e DB01_NAME=appdb \
117-
-e DB01_HOST=mysql8-ci \
118-
-e DB01_USER=root \
119-
-e DB01_PASS=rootpass \
120-
-e DB01_PORT=3306 \
121-
-e DB01_BACKUP_LOCATION=FILESYSTEM \
122-
"${IMAGE}" >/dev/null
123-
124-
sleep 10
125-
docker exec dbbackup-mysql backup-now >/dev/null
126-
127-
MYSQL_FILE="$(docker exec dbbackup-mysql sh -lc 'ls -1 /backup/mariadb_appdb_mysql8-ci_*.sql.zst | head -n1 | xargs -n1 basename')"
128-
test -n "${MYSQL_FILE}"
129-
130-
docker exec mysql8-ci mysql -uroot -prootpass -e "DROP DATABASE IF EXISTS restoredb; CREATE DATABASE restoredb;" >/dev/null
131-
docker exec dbbackup-mysql restore "/backup/${MYSQL_FILE}" mysql mysql8-ci restoredb root rootpass 3306 false >/dev/null
132-
MYSQL_COUNT="$(docker exec mysql8-ci mysql -N -uroot -prootpass -e "SELECT COUNT(*) FROM restoredb.ci_items;")"
133-
test "${MYSQL_COUNT}" = "2"
134-
135-
echo "=== Redis 7 backup smoke ==="
136-
docker run -d --name redis7-ci --network "${NET}" redis:7 >/dev/null
137-
sleep 5
138-
docker exec redis7-ci redis-cli SET ci_key ci_value >/dev/null
139-
140-
docker run -d --name dbbackup-redis --network "${NET}" \
141-
-v "${REDIS_BACKUP_DIR}:/backup" \
142-
-e MODE=MANUAL \
143-
-e CONTAINER_ENABLE_SCHEDULING=FALSE \
144-
-e CONTAINER_ENABLE_MONITORING=FALSE \
145-
-e MANUAL_RUN_FOREVER=TRUE \
146-
-e DB01_TYPE=redis \
147-
-e DB01_NAME=ALL \
148-
-e DB01_HOST=redis7-ci \
149-
-e DB01_PORT=6379 \
150-
-e DB01_BACKUP_LOCATION=FILESYSTEM \
151-
"${IMAGE}" >/dev/null
152-
153-
sleep 10
154-
docker exec dbbackup-redis backup-now >/dev/null
155-
docker exec dbbackup-redis sh -lc 'ls -1 /backup/redis_all_redis7-ci_*.rdb.zst >/dev/null'
156-
157-
echo "Integration tests passed."
31+
chmod +x scripts/smoke-test.sh
32+
IMAGE="a75g/docker-db-backup:ci" \
33+
NET="dbbackup-ci-net" \
34+
TMP_ROOT="${RUNNER_TEMP}/dbbackup-ci" \
35+
scripts/smoke-test.sh

Dockerfile

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ARG DISTRO_VARIANT=3.21-7.10.28
33
ARG ENABLE_INFLUX1_CLIENT=false
44
ARG ENABLE_MYSQL_SOURCE_CLIENT=false
55
ARG ENABLE_BLOBXFER=false
6+
ARG ENABLE_MSSQL_CLIENT=false
67

78
FROM docker.io/tiredofit/${DISTRO}:${DISTRO_VARIANT} AS compat
89

@@ -12,6 +13,7 @@ LABEL maintainer="Dave Conroy (github.com/tiredofit)"
1213
ARG ENABLE_INFLUX1_CLIENT
1314
ARG ENABLE_MYSQL_SOURCE_CLIENT
1415
ARG ENABLE_BLOBXFER
16+
ARG ENABLE_MSSQL_CLIENT
1517

1618
RUN apk add --no-cache bash ca-certificates curl
1719

@@ -49,7 +51,6 @@ RUN source /assets/functions/00-container && \
4951
addgroup -S -g 10000 dbbackup && \
5052
adduser -S -D -H -u 10000 -G dbbackup -g "Tired of I.T! DB Backup" dbbackup && \
5153
mkdir -p /usr/src /tmp/.container /backup /logs /tmp/backups && \
52-
\
5354
package update && \
5455
package upgrade && \
5556
echo '@edge_main https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && \
@@ -71,7 +72,6 @@ RUN source /assets/functions/00-container && \
7172
py3-pip \
7273
xz-dev \
7374
&& \
74-
\
7575
package install .db-backup-run-deps \
7676
bzip2 \
7777
coreutils \
@@ -110,38 +110,36 @@ RUN source /assets/functions/00-container && \
110110
apk add --no-cache --upgrade \
111111
--repository=https://dl-cdn.alpinelinux.org/alpine/edge/community \
112112
mongodb-tools redis && \
113-
apk add --no-cache postgresql18-client --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main && \
114-
\
113+
apk add --no-cache postgresql18-client --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main
114+
115+
RUN source /assets/functions/00-container && \
116+
set -ex && \
115117
case "$(uname -m)" in \
116-
"x86_64" ) mssql=true ; mssql_arch=amd64; influx2=true ; influx_arch=amd64; ;; \
117-
"arm64" | "aarch64" ) mssql=false ; influx2=true ; influx_arch=arm64 ;; \
118-
*) sleep 0.1 ;; \
119-
esac; \
120-
\
121-
if [ "${mssql,,}" = "true" ] ; then \
118+
"x86_64" ) mssql_arch=amd64; influx2=true ; influx_arch=amd64; ;; \
119+
"arm64" | "aarch64" ) mssql_arch=arm64; influx2=true ; influx_arch=arm64 ;; \
120+
*) mssql_arch=none; influx2=false ;; \
121+
esac && \
122+
if [ "${ENABLE_MSSQL_CLIENT,,}" = "true" ] && [ "${mssql_arch}" != "none" ]; then \
122123
curl -sSLO https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/msodbcsql18_${MSODBC_VERSION}_${mssql_arch}.apk ; \
123124
curl -sSLO https://download.microsoft.com/download/7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8/mssql-tools18_${MSSQL_VERSION}_${mssql_arch}.apk ; \
124125
echo y | apk add --allow-untrusted msodbcsql18_${MSODBC_VERSION}_${mssql_arch}.apk mssql-tools18_${MSSQL_VERSION}_${mssql_arch}.apk ; \
125126
else \
126-
echo >&2 "Detected non x86_64 or ARM64 build variant, skipping MSSQL installation" ; \
127-
fi; \
128-
\
127+
echo "Skipping optional MSSQL client installation" ; \
128+
fi && \
129129
if [ "${influx2,,}" = "true" ] ; then \
130130
curl -sSL https://dl.influxdata.com/influxdb/releases/influxdb2-client-${INFLUX2_CLIENT_VERSION}-linux-${influx_arch}.tar.gz | tar xvfz - --strip=1 -C /usr/src/ ; \
131131
chmod +x /usr/src/influx ; \
132132
mv /usr/src/influx /usr/sbin/ ; \
133133
else \
134-
echo >&2 "Unable to build Influx 2 on this system" ; \
135-
fi ; \
136-
\
134+
echo "Skipping optional InfluxDB v2 client install on this architecture" ; \
135+
fi && \
137136
if [ "${ENABLE_INFLUX1_CLIENT,,}" = "true" ] ; then \
138137
clone_git_repo https://github.com/influxdata/influxdb "${INFLUX1_CLIENT_VERSION}" && \
139138
go build -o /usr/sbin/influxd ./cmd/influxd && \
140139
strip /usr/sbin/influxd ; \
141140
else \
142141
echo "Skipping optional InfluxDB v1 client build" ; \
143142
fi && \
144-
\
145143
if [ "${ENABLE_MYSQL_SOURCE_CLIENT,,}" = "true" ] ; then \
146144
clone_git_repo "${MYSQL_REPO_URL}" "${MYSQL_VERSION}" && \
147145
cmake \
@@ -153,30 +151,28 @@ RUN source /assets/functions/00-container && \
153151
make -j$(nproc) install ; \
154152
else \
155153
echo "Skipping optional MySQL source client build (using mariadb-client)" ; \
156-
fi && \
157-
\
154+
fi
155+
156+
RUN source /assets/functions/00-container && \
157+
set -ex && \
158+
# Keep awscli pinned for deterministic S3 CLI behavior across architectures.
158159
pip3 install --break-system-packages awscli==${AWS_CLI_VERSION} && \
159160
if [ "${ENABLE_BLOBXFER,,}" = "true" ] ; then \
160161
pip3 install --break-system-packages blobxfer ; \
161162
else \
162163
echo "Skipping optional blobxfer installation" ; \
163164
fi && \
164-
\
165165
mkdir -p /usr/src/pbzip2 && \
166166
curl -sSL https://launchpad.net/pbzip2/1.1/1.1.13/+download/pbzip2-1.1.13.tar.gz | tar xvfz - --strip=1 -C /usr/src/pbzip2 && \
167167
cd /usr/src/pbzip2 && \
168168
make && \
169169
make install && \
170-
\
171-
# Fail the build immediately if any core backup client is unavailable.
172170
command -v pg_dump >/dev/null 2>&1 && \
173171
command -v psql >/dev/null 2>&1 && \
174172
command -v mysqldump >/dev/null 2>&1 && \
175173
command -v mongodump >/dev/null 2>&1 && \
176174
command -v redis-cli >/dev/null 2>&1 && \
177175
command -v sqlite3 >/dev/null 2>&1 && \
178-
\
179-
\
180176
package remove .db-backup-build-deps && \
181177
package cleanup && \
182178
rm -rf \

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ Optional build arguments (to reduce image size and attack surface):
125125
- `--build-arg ENABLE_INFLUX1_CLIENT=false` (default)
126126
- `--build-arg ENABLE_MYSQL_SOURCE_CLIENT=false` (default, uses `mariadb-client`)
127127
- `--build-arg ENABLE_BLOBXFER=false` (default)
128+
- `--build-arg ENABLE_MSSQL_CLIENT=false` (default; enable only if MSSQL backups/restores are needed)
129+
130+
Dependency policy notes:
131+
- `awscli` is pinned in the image build for deterministic S3/MinIO behavior across environments.
132+
- Optional clients are controlled with build args to reduce attack surface and CVE churn when unused.
128133

129134
### Prebuilt Images
130135

@@ -827,6 +832,15 @@ Manual backups can be performed by entering the container and typing `backup-now
827832

828833
- Recently there was a request to run the container with Kubernetes cron scheduling. This can be done by setting `MODE=MANUAL` and `MANUAL_RUN_FOREVER=FALSE` while disabling scheduler features from the upstream base image (`CONTAINER_ENABLE_SCHEDULING=FALSE`). This allows the container to start, execute backups, and then exit cleanly. An alternative way to run the script is `/etc/services.available/10-db-backup/run`.
829834

835+
### Local Smoke Test
836+
837+
Run the same end-to-end smoke suite used by CI:
838+
839+
```bash
840+
chmod +x scripts/smoke-test.sh
841+
IMAGE=a75g/docker-db-backup:ci scripts/smoke-test.sh
842+
```
843+
830844
### Restoring Databases
831845

832846
Entering the container and executing `restore` runs a menu-based script to restore backups. MariaDB, PostgreSQL, and MongoDB are supported.

install/assets/functions/10-db-backup

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -918,8 +918,12 @@ backup_redis() {
918918

919919
backup_sqlite3() {
920920
prepare_dbbackup
921-
db=$(basename "${backup_job_db_host}")
922-
db="${db%.*}"
921+
if [ -n "${backup_job_db_name}" ] && [ "${backup_job_db_name,,}" != "all" ]; then
922+
db="${backup_job_db_name}"
923+
else
924+
db=$(basename "${backup_job_db_host}")
925+
db="${db%.*}"
926+
fi
923927
backup_job_filename=sqlite3_${db}_${now}.sqlite3
924928
backup_job_filename_base=sqlite3_${db}
925929
pre_dbbackup "${db}"

0 commit comments

Comments
 (0)