mirror of
https://github.com/jkaninda/mysql-bkup.git
synced 2026-11-13 16:47:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dea2eedc35 | ||
|
|
28de0d7b52 | ||
|
|
ccb3fcdf26 | ||
|
|
fff1e1f9a9 | ||
|
|
e7a50743ed | ||
|
|
47220bf005 | ||
|
|
0ae9cffee0 | ||
|
|
0b08e0498a | ||
|
|
aef2395272 | ||
|
|
69f4795adb | ||
|
|
f5df2081f1 | ||
|
|
a1c991b363 | ||
|
|
416e094065 | ||
|
|
8ac6ffc394 | ||
|
|
59296c0c17 | ||
|
|
19b64bbad1 | ||
|
|
640e11b924 | ||
|
|
a746366348 | ||
|
|
64d57a077e | ||
|
|
995db83b7c | ||
|
|
5d5cbea60b | ||
|
|
2cb5603b88 | ||
|
|
548de725dd | ||
|
|
37b2867974 | ||
|
|
d6c01c9a4a | ||
|
|
bd92cc6844 | ||
|
|
3006abac3e | ||
|
|
3a16f6929c | ||
|
|
2f3ed7d0d8 | ||
|
|
a92bba05e4 | ||
|
|
7b565d54bd | ||
|
|
d10321dac6 | ||
|
|
d151c50caa | ||
|
|
07ecec57b3 | ||
|
|
bc4aab6ed0 | ||
|
|
12c17c18d6 | ||
|
|
80cd70e153 | ||
|
|
80abdb4299 | ||
|
|
72b21f00e6 | ||
|
|
3439c74257 | ||
|
|
4110ff4e64 | ||
|
|
cb299a35bf | ||
|
|
ba7c096bf3 | ||
|
|
6ac8dcef9e | ||
|
|
6bcc5d6bd4 | ||
|
|
fd35fabf97 | ||
|
|
23cce10e8c | ||
|
|
e666466d27 | ||
|
|
36bca254a9 | ||
|
|
ad18d42145 | ||
|
|
d9d44c2798 | ||
|
|
300a592508 | ||
|
|
be82e841e7 | ||
|
|
a73a365ebf | ||
|
|
75e965c0c5 | ||
|
|
fc60ddb308 | ||
|
|
573ef15ef3 | ||
|
|
b1776d3689 | ||
|
|
376d47f738 | ||
|
|
eb6268f8ec | ||
|
|
731e2d789d | ||
|
|
6300a8f2dd | ||
|
|
cd827a9277 | ||
|
|
71cf3fae85 | ||
|
|
528282bbd4 | ||
|
|
002c93a796 | ||
|
|
b6192f4c42 | ||
|
|
d5061453b0 | ||
|
|
0bc7497512 | ||
|
|
489dfdf842 | ||
|
|
907e70d552 | ||
|
|
696477fe5c | ||
|
|
56a8b51660 | ||
|
|
c76a00139c | ||
|
|
0f43871765 | ||
|
|
9ba6abe3f4 | ||
|
|
764583d88f | ||
|
|
dbf4dc596a | ||
|
|
06c89a9b78 | ||
|
|
ec8bdd806c | ||
|
|
828b11c6dd | ||
|
|
1d01e13909 | ||
|
|
bd65db2418 | ||
|
|
75b809511e | ||
|
|
fc028a2c55 | ||
|
|
7fa0c6a118 | ||
|
|
661702a97e | ||
|
|
dd5f33f17d | ||
|
|
b7cdfebd9c | ||
|
|
4b93becdf2 | ||
|
|
748cccec58 | ||
|
|
3e8bfabc44 | ||
|
|
777b59fd7c | ||
|
|
2b25f39c0a | ||
|
|
e5ba397bb4 | ||
|
|
3a1bfc512d | ||
|
|
b7b09ad6fd | ||
|
|
1206140a67 | ||
|
|
24573a96ad | ||
|
|
fff0b55722 | ||
|
|
68322e6b9f | ||
|
|
0f28772659 | ||
|
|
b95ccf3905 | ||
|
|
a06872834f | ||
|
|
393168c6c5 | ||
|
|
5b9ec8a224 | ||
|
|
2c3f2f4a46 | ||
|
|
0df14f37b4 | ||
|
|
1b60ca6fd2 | ||
|
|
d880f40108 | ||
|
|
c845b36797 | ||
|
|
63d615f838 | ||
|
|
6f31d35df2 | ||
|
|
f36d01cc96 | ||
|
|
07b7f54a75 | ||
|
|
7ff9a32f08 | ||
|
|
95a81cb6b7 | ||
|
|
057d5277b0 | ||
|
|
8e58d7a4c3 | ||
|
|
4bd7d9fa72 | ||
|
|
156f22f1e5 | ||
|
|
fd444293b4 | ||
|
|
1940ceba9a | ||
|
|
07d580a8a9 | ||
|
|
9a261b22ec | ||
|
|
e7a58f0569 | ||
|
|
1b529725d7 | ||
|
|
d8c73560b8 | ||
|
|
d5a0adc981 | ||
|
|
6df3bae9e2 | ||
|
|
f7d624fd15 | ||
|
|
1e9e1ed951 | ||
|
|
917ba8947f | ||
|
|
94a1dcdff7 | ||
|
|
f70e549b16 | ||
|
|
607478fcc6 | ||
|
|
2862e504f5 | ||
|
|
29420ee13e | ||
|
|
f53272ccf0 | ||
|
|
c360441445 | ||
|
|
f6916231f7 | ||
|
|
afd4afc83b | ||
|
|
9016a9ec7a | ||
|
|
4ecd96e75c | ||
|
|
8a88e4a727 | ||
|
|
62f86adea9 | ||
|
|
eb414d818c | ||
|
|
6721cc430d | ||
|
|
8e20e9595f | ||
|
|
02e3267237 | ||
|
|
448ef4d988 | ||
|
|
70ac78c2cd | ||
|
|
72f5ef4839 | ||
|
|
6a51f591a5 | ||
|
|
d55ade3c21 | ||
|
|
cdbd6dcd6a | ||
|
|
307e18d9ff | ||
|
|
8d366f0302 | ||
|
|
05e32c3cc1 | ||
|
|
edd13907d0 | ||
|
|
7cb1c50927 | ||
|
|
f545704b02 | ||
|
|
90f5391b24 | ||
|
|
ca241b4fef | ||
|
|
3911296921 | ||
|
|
8d04d276ba | ||
|
|
221079e0ea | ||
|
|
590b2d8bc6 | ||
|
|
d2aeb55ebc | ||
|
|
431be36210 | ||
|
|
ef2c5c80cd | ||
|
|
3a0137d6ea | ||
|
|
8afb5ace40 | ||
|
|
5569258a71 | ||
|
|
f3ec395e37 | ||
|
|
ba432997c8 | ||
|
|
dc20ea9635 | ||
|
|
40557af437 | ||
|
|
1dcb9586a6 | ||
|
|
2c6336e84a | ||
|
|
c16ee3a492 | ||
|
|
3f7d28ea49 | ||
|
|
cea1ef9c3b | ||
|
|
56c271bc29 | ||
|
|
45c30dca5f | ||
|
|
b0ae212578 | ||
|
|
6e2d3a9f21 | ||
|
|
dd314aa4cb | ||
|
|
24ccdaa671 | ||
|
|
45e3452376 | ||
|
|
3527b4cdcd | ||
|
|
dc6fe2f4b9 | ||
|
|
f0afc0f4e0 | ||
|
|
7d7c813bb0 | ||
|
|
6b8491cdc0 | ||
|
|
a1dd6e3f58 | ||
|
|
86ba3530c9 | ||
|
|
e1f3b15003 | ||
|
|
1577e92a66 | ||
|
|
7b67f88769 | ||
|
|
043233dabe | ||
|
|
d6652cfb75 | ||
|
|
140ed608ab | ||
|
|
98211a27b8 | ||
|
|
4e4d45e555 | ||
|
|
01e41acb5c | ||
|
|
3dce2017f8 | ||
|
|
ed2f1b8d9c | ||
|
|
b64875df21 | ||
|
|
fc90507b3f | ||
|
|
df0efd24d3 | ||
|
|
e5dd7e76ce | ||
|
|
12fbb67a09 | ||
|
|
df490af7b6 | ||
|
|
d930c3e2f6 | ||
|
|
e4258cb12e | ||
|
|
4c44166921 | ||
|
|
554df819ab | ||
|
|
ca5633882e | ||
|
|
c5cca82841 | ||
|
|
bbd5422089 | ||
|
|
d72156f890 | ||
|
|
909a50dbe7 | ||
|
|
94ceb71da2 | ||
|
|
fe05fe5110 | ||
|
|
dabba2050a | ||
|
|
47e1ac407b | ||
|
|
28f6ed3a82 | ||
|
|
504926c7cd | ||
|
|
737f473f92 | ||
|
|
300d2a8205 | ||
|
|
a4ad0502cf | ||
|
|
f344867edf | ||
|
|
d774584f64 | ||
|
|
96927cd57e | ||
|
|
ceacfa1d9d | ||
|
|
9380a18b45 | ||
|
|
d186071df9 | ||
|
|
71429b0e1a | ||
|
|
0bed86ded4 | ||
|
|
e891801125 | ||
|
|
01cf8a3392 | ||
|
|
efea81833a | ||
|
|
1cbf65d686 | ||
|
|
73d19913f8 | ||
|
|
b0224e43ef | ||
|
|
fa0485bb5a | ||
|
|
65ef6d3e8f | ||
|
|
a7b6abb101 | ||
|
|
3b21c109bc | ||
|
|
a50a1ef6f9 | ||
|
|
76bbfa35c4 | ||
|
|
599d93bef4 | ||
|
|
247e90f73e | ||
|
|
7d544aca68 | ||
|
|
1722ee0eeb | ||
|
|
726fd14831 | ||
|
|
fdc88e6064 | ||
|
|
2ba1b516e9 | ||
|
|
301594676b | ||
|
|
d06f2f2d7e | ||
|
|
2f06bd1c3a | ||
|
|
f383f5559d | ||
|
|
3725809d28 | ||
|
|
b1598ef7d0 | ||
|
|
e4a83b9851 | ||
|
|
4b2527f416 | ||
|
|
e97fc7512a | ||
|
|
7912ce46ed | ||
|
|
050f5e81bc | ||
|
|
b39e97b77d | ||
|
|
cbb73ae89b | ||
|
|
29a58aa26d | ||
|
|
041e0a07e9 | ||
|
|
9daac9c654 | ||
|
|
f6098769cd | ||
|
|
5cdfaa4d94 | ||
|
|
b205cd61ea | ||
|
|
e1307250e8 | ||
|
|
17ac951deb | ||
|
|
6e2e08224d | ||
|
|
570b775f48 | ||
|
|
e38e106983 | ||
|
|
3040420a09 | ||
|
|
eac5f70408 | ||
|
|
3476c6f529 | ||
|
|
1a9c8483f8 | ||
|
|
f8722f7ae4 | ||
|
|
421bf12910 | ||
|
|
3da4a27baa | ||
|
|
0881f075ef | ||
|
|
066e73f8e4 | ||
|
|
645243ff77 | ||
|
|
9384998127 | ||
|
|
390e7dad0c | ||
|
|
67ea22385f | ||
|
|
cde82d8cfc | ||
|
|
4808f093e5 | ||
|
|
c7a03861fe | ||
|
|
36ec63d522 | ||
|
|
0f07de1d83 | ||
|
|
ae55839996 | ||
|
|
a7f7e57a0d | ||
|
|
b2ddaec93b | ||
|
|
b3570d774c | ||
|
|
38f7e91c03 | ||
|
|
07c2935925 | ||
|
|
f3c5585051 | ||
|
|
7163d030a5 | ||
|
|
a2cec86e73 | ||
|
|
662b73579d | ||
|
|
c9f8a32de1 | ||
|
|
8fb008151c | ||
|
|
113c84c885 | ||
|
|
58deb92953 | ||
|
|
c41afb8b57 | ||
|
|
02e51a3933 | ||
|
|
db4061b64b | ||
|
|
9467b157aa | ||
|
|
c229ebdc9d | ||
|
|
7b701d1740 | ||
|
|
ad6f190bad | ||
|
|
de4dcaaeca | ||
|
|
17c0a99bda | ||
|
|
b1c9abf931 | ||
|
|
a70a893c11 | ||
|
|
243e25f4fb | ||
|
|
cb0dcf4104 | ||
|
|
d26d8d31c9 | ||
|
|
71d438ba76 | ||
|
|
a3fc58af96 | ||
|
|
08ca6d4a39 | ||
|
|
27b9ab5f36 | ||
|
|
6d6db7061b | ||
|
|
d90647aae7 | ||
|
|
5c2c05499f | ||
|
|
88ada6fefd | ||
|
|
e6c8b0923d | ||
|
|
59a136039c | ||
|
|
db835e81c4 | ||
|
|
5b05bcbf0c | ||
|
|
b8277c8464 |
No files matched your search
@@ -0,0 +1,30 @@
|
|||||||
|
# Only the Go sources are needed to build the image.
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.idea
|
||||||
|
.history
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.env
|
||||||
|
backup/
|
||||||
|
backups/
|
||||||
|
data/
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
|
vendor/
|
||||||
|
docs/
|
||||||
|
examples/
|
||||||
|
migrations/
|
||||||
|
test.md
|
||||||
|
compose.yaml
|
||||||
|
docker-compose*.yaml
|
||||||
|
# Binaries and dumps
|
||||||
|
mysql-bkup
|
||||||
|
mysql-bkup-*
|
||||||
|
*.sql
|
||||||
|
*.sql.gz
|
||||||
|
*.gpg
|
||||||
|
*.age
|
||||||
|
*.tar
|
||||||
|
*.gz
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
### Database
|
||||||
|
DB_HOST=
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=
|
||||||
|
#DB_SSL_MODE=disabled
|
||||||
|
TZ=Europe/Paris
|
||||||
|
|
||||||
|
### Database Migration
|
||||||
|
#TARGET_DB_HOST=
|
||||||
|
#TARGET_DB_PORT=3306
|
||||||
|
#TARGET_DB_NAME=
|
||||||
|
#DB_SSL_MODE=disabled
|
||||||
|
#TARGET_DB_USER=
|
||||||
|
#TARGET_DB_PASSWORD=
|
||||||
|
|
||||||
|
### Backup restoration
|
||||||
|
#FILE_NAME=
|
||||||
|
|
||||||
|
### AWS S3 Storage
|
||||||
|
#AWS_ACCESS_KEY=
|
||||||
|
#AWS_SECRET_KEY=
|
||||||
|
#AWS_S3_BUCKET_NAME=
|
||||||
|
#AWS_S3_ENDPOINT=
|
||||||
|
#AWS_REGION=
|
||||||
|
#REMOTE_PATH=
|
||||||
|
#AWS_DISABLE_SSL=false
|
||||||
|
#AWS_FORCE_PATH_STYLE=true
|
||||||
|
|
||||||
|
### Backup Cron Expression
|
||||||
|
#BACKUP_CRON_EXPRESSION=@midnight
|
||||||
|
##Delete old backup created more than specified days ago
|
||||||
|
#BACKUP_RETENTION_DAYS=7
|
||||||
|
#BACKUP_RETENTION_COUNT=10
|
||||||
|
|
||||||
|
####SSH Storage
|
||||||
|
#SFTP_HOST=
|
||||||
|
#SFTP_PORT=22
|
||||||
|
#SFTP_USER=
|
||||||
|
#SFTP_PASSWORD=
|
||||||
|
#SFTP_IDENTITY_FILE=/tmp/id_ed25519
|
||||||
|
#SFTP_KNOWN_HOSTS=/config/known_hosts
|
||||||
|
|
||||||
|
####FTP Storage
|
||||||
|
#FTP_PASSWORD=
|
||||||
|
#FTP_HOST=
|
||||||
|
#FTP_USER=
|
||||||
|
#FTP_PORT=21
|
||||||
|
#REMOTE_PATH=
|
||||||
|
|
||||||
|
## Azure Blob storage
|
||||||
|
#AZURE_STORAGE_CONTAINER_NAME=
|
||||||
|
#AZURE_STORAGE_ACCOUNT_NAME=
|
||||||
|
#AZURE_STORAGE_ACCOUNT_KEY=
|
||||||
|
|
||||||
|
#### Backup encryption
|
||||||
|
#GPG_PUBLIC_KEY=/config/gnupg/public_key.asc
|
||||||
|
#GPG_PRIVATE_KEY=/config/gnupg/private_key.asc
|
||||||
|
#AGE_RECIPIENTS=age1...
|
||||||
|
#AGE_IDENTITY_FILE=/config/age/identity.txt
|
||||||
|
#GPG_PASSPHRASE=Your strong passphrase
|
||||||
|
|
||||||
|
## For multiple database backup on Docker or Docker in Swarm mode
|
||||||
|
#BACKUP_CONFIG_FILE=/config/config.yaml
|
||||||
|
|
||||||
|
### Database restoration
|
||||||
|
#FILE_NAME=
|
||||||
|
|
||||||
|
### Notification
|
||||||
|
#BACKUP_REFERENCE=K8s/Paris cluster
|
||||||
|
|
||||||
|
## Telegram
|
||||||
|
#TG_TOKEN=
|
||||||
|
#TG_CHAT_ID=
|
||||||
|
|
||||||
|
### Email
|
||||||
|
#MAIL_HOST=
|
||||||
|
#MAIL_PORT=
|
||||||
|
#MAIL_USERNAME=
|
||||||
|
#MAIL_PASSWORD=
|
||||||
|
#MAIL_FROM=Backup Jobs <backup-jobs@example.com>
|
||||||
|
#MAIL_TO=backup@example.com,me@example.com,team@example.com
|
||||||
|
#MAIL_TLS=starttls
|
||||||
|
#MAIL_INSECURE_SKIP_VERIFY=false
|
||||||
|
|
||||||
|
## Full reference: mysql-bkup config --env-docs
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
ko_fi: jkaninda
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: docker
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
name: Build
|
name: Build
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ['develop']
|
branches: ['nightly']
|
||||||
env:
|
env:
|
||||||
BUILDKIT_IMAGE: jkaninda/mysql-bkup
|
BUILDKIT_IMAGE: jkaninda/mysql-bkup
|
||||||
jobs:
|
jobs:
|
||||||
@@ -10,23 +10,25 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
file: "./docker/Dockerfile"
|
file: "./Dockerfile"
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
build-args: |
|
||||||
|
appVersion=nightly
|
||||||
tags: |
|
tags: |
|
||||||
"${{env.BUILDKIT_IMAGE}}:develop-${{ github.sha }}"
|
"${{ vars.BUILDKIT_IMAGE || 'jkaninda/mysql-bkup' }}:nightly"
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v7
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
@@ -32,14 +32,14 @@ jobs:
|
|||||||
working-directory: docs
|
working-directory: docs
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
id: pages
|
id: pages
|
||||||
uses: actions/configure-pages@v2
|
uses: actions/configure-pages@v6
|
||||||
- name: Build with Jekyll
|
- name: Build with Jekyll
|
||||||
working-directory: docs
|
working-directory: docs
|
||||||
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
|
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
|
||||||
env:
|
env:
|
||||||
JEKYLL_ENV: production
|
JEKYLL_ENV: production
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: 'docs/_site/'
|
path: 'docs/_site/'
|
||||||
|
|
||||||
@@ -52,4 +52,4 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v1
|
uses: actions/deploy-pages@v5
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: golangci-lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone the code
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
|
||||||
|
- name: Run linter
|
||||||
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
with:
|
||||||
|
version: v2.15.0
|
||||||
|
args: --build-tags integration
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Release
|
name: CI
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -14,18 +14,18 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -34,16 +34,29 @@ jobs:
|
|||||||
name: Get the tag name
|
name: Get the tag name
|
||||||
id: get_tag_name
|
id: get_tag_name
|
||||||
run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
-
|
||||||
|
name: Image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
${{ vars.BUILDKIT_IMAGE || 'jkaninda/mysql-bkup' }}
|
||||||
|
ghcr.io/${{ vars.BUILDKIT_IMAGE || 'jkaninda/mysql-bkup' }}
|
||||||
|
# v2.1.3 -> v2.1.3, 2.1.3, 2.1, 2 and latest (immutable semver tags next to latest)
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ env.TAG_NAME }}
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
type=raw,value=latest
|
||||||
-
|
-
|
||||||
name: Build and push
|
name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
file: "./docker/Dockerfile"
|
file: "./Dockerfile"
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
tags: |
|
build-args: |
|
||||||
"${{env.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
|
appVersion=${{ env.TAG_NAME }}
|
||||||
"${{env.BUILDKIT_IMAGE}}:latest"
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
"ghcr.io/${{env.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
"ghcr.io/${{env.BUILDKIT_IMAGE}}:latest"
|
|
||||||
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- nightly
|
||||||
|
- v2
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
unit:
|
||||||
|
name: Vet and unit tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- name: Vet
|
||||||
|
run: |
|
||||||
|
go vet ./...
|
||||||
|
go vet -tags integration ./...
|
||||||
|
- name: Unit tests
|
||||||
|
run: go test -race ./...
|
||||||
|
- name: Env reference is up to date
|
||||||
|
run: go run . config --env-docs > /dev/null
|
||||||
|
|
||||||
|
integration:
|
||||||
|
name: Integration tests (MySQL 8.4 / 8.0, MariaDB 11.8, MinIO)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- uses: actions/setup-go@v7
|
||||||
|
with:
|
||||||
|
go-version: '1.26'
|
||||||
|
- uses: docker/setup-buildx-action@v4
|
||||||
|
- name: Build the image
|
||||||
|
run: docker buildx build --build-arg appVersion=test -t mysql-bkup:test --load .
|
||||||
|
- name: Image smoke test
|
||||||
|
run: |
|
||||||
|
docker run --rm mysql-bkup:test version
|
||||||
|
docker run --rm --entrypoint mysqldump mysql-bkup:test --version
|
||||||
|
docker run --rm --entrypoint mariadb-dump mysql-bkup:test --version
|
||||||
|
# Miabi deletes local backups with /bin/sh
|
||||||
|
docker run --rm --entrypoint /bin/sh mysql-bkup:test -c "rm -f /backup/x.sql.gz"
|
||||||
|
- name: Integration tests (client tools from the image)
|
||||||
|
env:
|
||||||
|
IT_CLIENT_IMAGE: mysql-bkup:test
|
||||||
|
run: go test -tags integration -count=1 -timeout 40m -v ./...
|
||||||
@@ -9,3 +9,4 @@ mysql-bkup
|
|||||||
/.DS_Store
|
/.DS_Store
|
||||||
/.idea
|
/.idea
|
||||||
bin
|
bin
|
||||||
|
Makefile
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
version: "2"
|
||||||
|
run:
|
||||||
|
allow-parallel-runners: true
|
||||||
|
build-tags:
|
||||||
|
- integration
|
||||||
|
linters:
|
||||||
|
default: none
|
||||||
|
enable:
|
||||||
|
- copyloopvar
|
||||||
|
- dupl
|
||||||
|
- errcheck
|
||||||
|
- goconst
|
||||||
|
- gocyclo
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- lll
|
||||||
|
- misspell
|
||||||
|
- nakedret
|
||||||
|
- prealloc
|
||||||
|
- revive
|
||||||
|
- staticcheck
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- unused
|
||||||
|
settings:
|
||||||
|
goconst:
|
||||||
|
min-occurrences: 4
|
||||||
|
lll:
|
||||||
|
line-length: 160
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: comment-spacings
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
rules:
|
||||||
|
- linters:
|
||||||
|
- dupl
|
||||||
|
- lll
|
||||||
|
- goconst
|
||||||
|
- errcheck
|
||||||
|
- prealloc
|
||||||
|
path: _test\.go
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
exclusions:
|
||||||
|
generated: lax
|
||||||
|
paths:
|
||||||
|
- third_party$
|
||||||
|
- builtin$
|
||||||
|
- examples$
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
ARG GO_IMAGE=golang:1.26.3-trixie
|
||||||
|
ARG RUNTIME_IMAGE=debian:13.7-slim
|
||||||
|
ARG MYSQL_IMAGE=mysql:8.4.13
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM ${GO_IMAGE} AS build
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
ARG appVersion="development"
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN GOARM="${TARGETVARIANT#v}" CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
|
||||||
|
go build -trimpath -ldflags="-s -w -X main.Version=${appVersion}" -o /out/mysql-bkup .
|
||||||
|
|
||||||
|
# Oracle MySQL client binaries, per architecture.
|
||||||
|
FROM ${MYSQL_IMAGE} AS mysqlclient-amd64
|
||||||
|
RUN mkdir -p /out && cp /usr/bin/mysql /usr/bin/mysqldump /out/
|
||||||
|
FROM ${MYSQL_IMAGE} AS mysqlclient-arm64
|
||||||
|
RUN mkdir -p /out && cp /usr/bin/mysql /usr/bin/mysqldump /out/
|
||||||
|
FROM ${RUNTIME_IMAGE} AS mysqlclient-arm
|
||||||
|
RUN mkdir -p /out
|
||||||
|
FROM mysqlclient-${TARGETARCH} AS mysqlclient
|
||||||
|
|
||||||
|
FROM ${RUNTIME_IMAGE}
|
||||||
|
ARG appVersion="development"
|
||||||
|
ENV TZ=UTC
|
||||||
|
LABEL org.opencontainers.image.title="mysql-bkup" \
|
||||||
|
org.opencontainers.image.description="Back up and restore MySQL and MariaDB databases to local storage, S3, GCS, Azure, SFTP or FTP, with GPG or age encryption" \
|
||||||
|
org.opencontainers.image.licenses="MIT" \
|
||||||
|
org.opencontainers.image.authors="Jonas Kaninda <me@jkaninda.dev>" \
|
||||||
|
org.opencontainers.image.version=${appVersion} \
|
||||||
|
org.opencontainers.image.source="https://github.com/jkaninda/mysql-bkup"
|
||||||
|
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
mariadb-client-core libssl3t64 zlib1g libncurses6 libtinfo6 \
|
||||||
|
tini tzdata ca-certificates \
|
||||||
|
&& cd /tmp && apt-get download mariadb-client \
|
||||||
|
&& dpkg-deb --fsys-tarfile mariadb-client_*.deb | tar -x -C / ./usr/bin/mariadb-dump \
|
||||||
|
&& rm -f mariadb-client_*.deb \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# /usr/local/bin comes first in PATH: mysql and mysqldump are Oracle's there,
|
||||||
|
# while Debian's /usr/bin/mysql* are links to the MariaDB tools.
|
||||||
|
COPY --from=mysqlclient /out/ /usr/local/bin/
|
||||||
|
COPY --from=build /out/mysql-bkup /usr/local/bin/mysql-bkup
|
||||||
|
|
||||||
|
|
||||||
|
RUN mkdir -p /config /backup /tmp/backup \
|
||||||
|
&& chmod 1777 /backup /tmp/backup \
|
||||||
|
&& ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup \
|
||||||
|
&& printf '#!/bin/sh\nexec /usr/local/bin/mysql-bkup backup "$@"\n' > /usr/local/bin/backup \
|
||||||
|
&& printf '#!/bin/sh\nexec /usr/local/bin/mysql-bkup restore "$@"\n' > /usr/local/bin/restore \
|
||||||
|
&& printf '#!/bin/sh\nexec /usr/local/bin/mysql-bkup migrate "$@"\n' > /usr/local/bin/migrate \
|
||||||
|
&& chmod +x /usr/local/bin/backup /usr/local/bin/restore /usr/local/bin/migrate \
|
||||||
|
&& (mysqldump --version 2>/dev/null || true) && mariadb-dump --version && mysql-bkup version
|
||||||
|
|
||||||
|
WORKDIR /config
|
||||||
|
HEALTHCHECK --interval=1m --timeout=10s --start-period=1m CMD ["/usr/local/bin/mysql-bkup", "health"]
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/mysql-bkup"]
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
BINARY_NAME=mysql-bkup
|
|
||||||
include .env
|
|
||||||
export
|
|
||||||
run:
|
|
||||||
go run . backup
|
|
||||||
|
|
||||||
build:
|
|
||||||
go build -o bin/${BINARY_NAME} .
|
|
||||||
|
|
||||||
compile:
|
|
||||||
GOOS=darwin GOARCH=arm64 go build -o bin/${BINARY_NAME}-darwin-arm64 .
|
|
||||||
GOOS=darwin GOARCH=amd64 go build -o bin/${BINARY_NAME}-darwin-amd64 .
|
|
||||||
GOOS=linux GOARCH=arm64 go build -o bin/${BINARY_NAME}-linux-arm64 .
|
|
||||||
GOOS=linux GOARCH=amd64 go build -o bin/${BINARY_NAME}-linux-amd64 .
|
|
||||||
|
|
||||||
docker-build:
|
|
||||||
docker build -f docker/Dockerfile -t jkaninda/mysql-bkup:latest .
|
|
||||||
|
|
||||||
docker-run: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -v "./backup:/backup" -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup backup --prune --keep-last 2
|
|
||||||
docker-restore: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -v "./backup:/backup" -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup restore -f ${FILE_NAME}
|
|
||||||
|
|
||||||
|
|
||||||
docker-run-scheduled: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -v "./backup:/backup" -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup backup --mode scheduled --period "* * * * *"
|
|
||||||
|
|
||||||
|
|
||||||
docker-run-scheduled-s3: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -v "./backup:/backup" -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "ACCESS_KEY=${ACCESS_KEY}" -e "SECRET_KEY=${SECRET_KEY}" -e "BUCKET_NAME=${BUCKET_NAME}" -e "S3_ENDPOINT=${AWS_S3_ENDPOINT}" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup backup --storage s3 --mode scheduled --path /custom-path --period "* * * * *"
|
|
||||||
|
|
||||||
docker-run-s3: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "ACCESS_KEY=${ACCESS_KEY}" -e "SECRET_KEY=${SECRET_KEY}" -e "AWS_S3_BUCKET_NAME=${AWS_S3_BUCKET_NAME}" -e "AWS_S3_ENDPOINT=${AWS_S3_ENDPOINT}" -e "AWS_REGION=eu2" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup backup --storage s3 --path /custom-path
|
|
||||||
|
|
||||||
|
|
||||||
docker-restore-s3: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "ACCESS_KEY=${ACCESS_KEY}" -e "SECRET_KEY=${SECRET_KEY}" -e "BUCKET_NAME=${AWS_S3_BUCKET_NAME}" -e "S3_ENDPOINT=${AWS_S3_ENDPOINT}" -e "AWS_REGION=eu2" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup restore --storage s3 -f ${FILE_NAME} --path /custom-path
|
|
||||||
|
|
||||||
docker-run-ssh: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "SSH_USER=${SSH_USER}" -e "SSH_HOST_NAME=${SSH_HOST_NAME}" -e "SSH_REMOTE_PATH=${SSH_REMOTE_PATH}" -e "SSH_PASSWORD=${SSH_PASSWORD}" -e "SSH_PORT=${SSH_PORT}" -e "SSH_IDENTIFY_FILE=${SSH_IDENTIFY_FILE}" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" jkaninda/mysql-bkup bkup backup --storage ssh
|
|
||||||
|
|
||||||
docker-restore-ssh: docker-build
|
|
||||||
docker run --rm --network web --name mysql-bkup -e "DB_HOST=${DB_HOST}" -e "DB_NAME=${DB_NAME}" -e "DB_USERNAME=${DB_USERNAME}" -e "DB_PASSWORD=${DB_PASSWORD}" -e "SSH_USER=${SSH_USER}" -e "SSH_HOST_NAME=${SSH_HOST_NAME}" -e "SSH_REMOTE_PATH=${SSH_REMOTE_PATH}" -e "SSH_PASSWORD=${SSH_PASSWORD}" -e "SSH_PORT=${SSH_PORT}" -e "GPG_PASSPHRASE=${GPG_PASSPHRASE}" -e "SSH_IDENTIFY_FILE=${SSH_IDENTIFY_FILE}" jkaninda/mysql-bkup bkup restore --storage ssh -f ${FILE_NAME}
|
|
||||||
|
|
||||||
run-docs:
|
|
||||||
cd docs && bundle exec jekyll serve -H 0.0.0.0 -t
|
|
||||||
@@ -1,170 +1,447 @@
|
|||||||
# MySQL Backup
|
# MYSQL-BKUP
|
||||||
mysql-bkup is a Docker container image that can be used to backup and restore Postgres database. It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, and SSH compatible storage.
|
|
||||||
It also supports __encrypting__ your backups using GPG.
|
|
||||||
|
|
||||||
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.
|
**MYSQL-BKUP** backs up, restores and migrates **MySQL and MariaDB** databases. It runs as a Docker image (one-shot, as a scheduled daemon, or as a Kubernetes Job/CronJob) and streams every backup end to end: dump, compression, encryption and upload happen in one pipe, without temporary dump files and with a fixed memory footprint, whatever the database size.
|
||||||
It handles __recurring__ backups of postgres database on Docker and can be deployed as __CronJob on Kubernetes__ using local, AWS S3 or SSH compatible storage.
|
|
||||||
|
|
||||||
It also supports __encrypting__ your backups using GPG.
|
|
||||||
|
|
||||||
|
[](https://github.com/jkaninda/mysql-bkup/actions/workflows/tests.yml)
|
||||||
[](https://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml)
|
[](https://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml)
|
||||||
[](https://goreportcard.com/report/github.com/jkaninda/mysql-bkup)
|
[](https://goreportcard.com/report/github.com/jkaninda/mysql-bkup)
|
||||||

|

|
||||||

|

|
||||||
|
<a href="https://ko-fi.com/jkaninda"><img src="https://uploads-ssl.webflow.com/5c14e387dab576fe667689cf/5cbed8a4ae2b88347c06c923_BuyMeACoffee_blue.png" height="20" alt="buy ma a coffee"></a>
|
||||||
|
|
||||||
- Docker
|
Documentation: <https://jkaninda.github.io/mysql-bkup>
|
||||||
- Kubernetes
|
|
||||||
|
|
||||||
## Documentation is found at <https://jkaninda.github.io/mysql-bkup>
|
## Features
|
||||||
|
|
||||||
|
- **MySQL and MariaDB, each with its own tools.** The server flavour is detected (`SELECT VERSION()`): MariaDB is dumped with `mariadb-dump` and restored with `mariadb`, MySQL 8+ with Oracle's `mysqldump` and `mysql` (8.4 LTS client). Tested against MySQL 8.0, MySQL 8.4 and MariaDB 11.8.
|
||||||
|
- **Storage:** local, Amazon S3 and S3-compatible (MinIO, Wasabi, R2, ...), Google Cloud Storage, Azure Blob, SFTP, FTP/FTPS. Extra copies to more storages from the config file.
|
||||||
|
- **Encryption:** GPG (passphrase or public key, the default and compatible with 1.x) or [age](https://age-encryption.org) (recipients or passphrase). Encryption fails closed: a configured key that cannot be loaded is an error, never a silent plaintext upload.
|
||||||
|
- **Compression:** gzip (default, parallel) or zstd; restore detects the format.
|
||||||
|
- **Backup index and safe retention:** every storage folder has an index (`mysql-bkup-metadata.json`) and a `.meta.json` sidecar per backup with its sha256. Retention by age and/or count only deletes backups the index knows, and always keeps the newest one.
|
||||||
|
- **Commands:** `backup`, `restore`, `migrate`, `verify`, `list`, `delete`, `prune`, `reconcile`, `config`, `health`.
|
||||||
|
- **Scheduling:** cron expressions (5 or 6 fields, `@daily`, `@every 6h`), no overlapping runs, clean shutdown on SIGTERM, Prometheus metrics, Docker `HEALTHCHECK`.
|
||||||
|
- **Notifications:** email, Telegram, Slack, Discord, ntfy, generic webhooks (HMAC-signed), healthchecks.io-style pings.
|
||||||
|
- **Security:** no shell anywhere; database passwords go to the client tools through a 0600 option file in a private directory (never on the command line), and every secret can be read from `<NAME>_FILE` (Docker/Kubernetes secrets).
|
||||||
|
- **Multi-arch image:** `amd64`, `arm64`, `arm/v7` (see [Client tools](#client-tools)).
|
||||||
|
|
||||||
## Links:
|
## Quick start
|
||||||
|
|
||||||
- [Docker Hub](https://hub.docker.com/r/jkaninda/mysql-bkup)
|
Back up the database `shop` into `./backup`:
|
||||||
- [Github](https://github.com/jkaninda/mysql-bkup)
|
|
||||||
|
|
||||||
## PostgreSQL solution :
|
|
||||||
|
|
||||||
- [PostgreSQL](https://github.com/jkaninda/pg-bkup)
|
|
||||||
|
|
||||||
## Storage:
|
|
||||||
- Local
|
|
||||||
- AWS S3 or any S3 Alternatives for Object Storage
|
|
||||||
- SSH
|
|
||||||
|
|
||||||
## Quickstart
|
|
||||||
|
|
||||||
### Simple backup using Docker CLI
|
|
||||||
|
|
||||||
To run a one time backup, bind your local volume to `/backup` in the container and run the `mysql-bkup backup` command:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker run --rm --network your_network_name \
|
docker run --rm --network your_network \
|
||||||
-v $PWD/backup:/backup/ \
|
-v $PWD/backup:/backup \
|
||||||
-e "DB_HOST=dbhost" \
|
-e DB_HOST=mysql -e DB_USER=root -e DB_PASSWORD=password \
|
||||||
-e "DB_USERNAME=username" \
|
jkaninda/mysql-bkup backup -d shop
|
||||||
-e "DB_PASSWORD=password" \
|
|
||||||
jkaninda/mysql-bkup mysql-bkup backup -d database_name
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, pass a `--env-file` in order to use a full config as described below.
|
The last line of the output names the backup:
|
||||||
|
|
||||||
|
```
|
||||||
|
level=INFO msg="Backup completed" database=shop file=shop_20261027_031500.sql.gz size="1.2 MiB" duration=2.1s storage=local location=/backup
|
||||||
|
```
|
||||||
|
|
||||||
### Simple backup in docker compose file
|
Restore it:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm --network your_network \
|
||||||
|
-v $PWD/backup:/backup \
|
||||||
|
-e DB_HOST=mysql -e DB_USER=root -e DB_PASSWORD=password \
|
||||||
|
jkaninda/mysql-bkup restore -d shop -f shop_20261027_031500.sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore accepts mysql-bkup backups and any plain `.sql` or gzip `.sql.gz` dump (encrypted or not). A missing database is created first.
|
||||||
|
|
||||||
|
### Docker Compose, scheduled
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
|
||||||
# release version instead of using `latest`.
|
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
command: backup -d shop --cron-expression "@daily"
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=5432
|
- DB_HOST=mysql
|
||||||
- DB_HOST=postgres
|
- DB_USER=backup
|
||||||
- DB_NAME=foo
|
- DB_PASSWORD_FILE=/run/secrets/db_password
|
||||||
- DB_USERNAME=bar
|
- BACKUP_RETENTION_DAYS=7
|
||||||
- DB_PASSWORD=password
|
- GPG_PASSPHRASE=change-me
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
networks:
|
|
||||||
web:
|
|
||||||
```
|
```
|
||||||
## Deploy on Kubernetes
|
|
||||||
|
|
||||||
For Kubernetes, you don't need to run it in scheduled mode. You can deploy it as CronJob.
|
### Kubernetes CronJob
|
||||||
|
|
||||||
### Simple Kubernetes CronJob usage:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: CronJob
|
kind: CronJob
|
||||||
metadata:
|
metadata:
|
||||||
name: bkup-job
|
name: mysql-bkup
|
||||||
spec:
|
spec:
|
||||||
schedule: "0 1 * * *"
|
schedule: "0 1 * * *"
|
||||||
jobTemplate:
|
jobTemplate:
|
||||||
spec:
|
spec:
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
containers:
|
containers:
|
||||||
- name: mysql-bkup
|
- name: mysql-bkup
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
command:
|
args: ["backup", "-d", "shop", "--storage", "s3", "--path", "/mysql"]
|
||||||
- /bin/sh
|
envFrom:
|
||||||
- -c
|
- secretRef:
|
||||||
- mysql-bkup backup -s s3 --path /custom_path
|
name: mysql-bkup # DB_HOST, DB_USER, DB_PASSWORD, AWS_* ...
|
||||||
env:
|
|
||||||
- name: DB_PORT
|
|
||||||
value: "5432"
|
|
||||||
- name: DB_HOST
|
|
||||||
value: ""
|
|
||||||
- name: DB_NAME
|
|
||||||
value: ""
|
|
||||||
- name: DB_USERNAME
|
|
||||||
value: ""
|
|
||||||
# Please use secret!
|
|
||||||
- name: DB_PASSWORD
|
|
||||||
value: ""
|
|
||||||
- name: AWS_S3_ENDPOINT
|
|
||||||
value: "https://s3.amazonaws.com"
|
|
||||||
- name: AWS_S3_BUCKET_NAME
|
|
||||||
value: "xxx"
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: "us-west-2"
|
|
||||||
- name: AWS_ACCESS_KEY
|
|
||||||
value: "xxxx"
|
|
||||||
- name: AWS_SECRET_KEY
|
|
||||||
value: "xxxx"
|
|
||||||
- name: AWS_DISABLE_SSL
|
|
||||||
value: "false"
|
|
||||||
restartPolicy: Never
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `backup` | Back up now, or as a daemon with `--cron-expression` / `BACKUP_CRON_EXPRESSION`. `-d DB`, `-a` (every database, one file each), `-A` (every database in one file), `--exclude-db`, `--custom-name`, `--disable-compression`, `-c config.yaml` |
|
||||||
|
| `restore` | Stream a backup back: `-f FILE [-d DB]`. Without `-d`, the dump's own `USE` statements decide (all-in-one backups) |
|
||||||
|
| `migrate` | Copy `DB_NAME` (or every database with `-a`) straight into `TARGET_DB_*`: `dump \| restore` through a pipe, no file |
|
||||||
|
| `verify` | Download, decrypt and decompress a backup and check its sha256, without restoring |
|
||||||
|
| `list` | Backup history from the index (`--json`, `--storage-scan`) |
|
||||||
|
| `delete` | Delete a backup, its sidecar and its index entry |
|
||||||
|
| `prune` | Apply the retention policy (`--dry-run`) |
|
||||||
|
| `reconcile` | Rebuild the index from the sidecars (`--from-index OLD` after renaming it) |
|
||||||
|
| `config` | Show the resolved configuration with sources, secrets redacted; `--env-docs` prints the reference below |
|
||||||
|
| `health` | Docker `HEALTHCHECK` for the daemon |
|
||||||
|
| `version` | Print the version |
|
||||||
|
|
||||||
|
The image also has the `backup`, `restore` and `migrate` shortcuts (`docker run ... --entrypoint backup`, or in Kubernetes `command: [backup]`) and the `bkup` alias.
|
||||||
|
|
||||||
|
### Exit codes
|
||||||
|
|
||||||
|
`0` success, `1` failure, `2` configuration error, `3` database connection or authentication, `4` storage, `5` encryption/decryption.
|
||||||
|
|
||||||
|
## Storage
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# S3 / MinIO
|
||||||
|
-e AWS_S3_ENDPOINT=https://s3.amazonaws.com -e AWS_S3_BUCKET_NAME=backups -e AWS_REGION=us-east-1 \
|
||||||
|
-e AWS_ACCESS_KEY=... -e AWS_SECRET_KEY=... \
|
||||||
|
jkaninda/mysql-bkup backup -d shop --storage s3 --path /mysql
|
||||||
|
# MinIO and other S3 alternatives: add AWS_FORCE_PATH_STYLE=true (and AWS_DISABLE_SSL=true for plain http).
|
||||||
|
# Without AWS_ACCESS_KEY, the AWS default credential chain is used (IAM role, IRSA, profile).
|
||||||
|
|
||||||
|
# SFTP (host keys are verified: mount a known_hosts file)
|
||||||
|
-e SFTP_HOST=backup.example.com -e SFTP_USER=backup -e SFTP_IDENTITY_FILE=/config/id_ed25519 \
|
||||||
|
-e SFTP_KNOWN_HOSTS=/config/known_hosts \
|
||||||
|
jkaninda/mysql-bkup backup -d shop --storage sftp --path /home/backup/mysql
|
||||||
|
|
||||||
|
# FTP / FTPS
|
||||||
|
-e FTP_HOST=ftp.example.com -e FTP_USER=backup -e FTP_PASSWORD=... -e FTP_TLS=true \
|
||||||
|
jkaninda/mysql-bkup backup -d shop --storage ftp --path /mysql
|
||||||
|
|
||||||
|
# Azure Blob
|
||||||
|
-e AZURE_STORAGE_ACCOUNT_NAME=... -e AZURE_STORAGE_ACCOUNT_KEY=... -e AZURE_STORAGE_CONTAINER_NAME=backups \
|
||||||
|
jkaninda/mysql-bkup backup -d shop --storage azure --path mysql
|
||||||
|
|
||||||
|
# Google Cloud Storage
|
||||||
|
-e GCS_BUCKET=backups -e GCS_CREDENTIALS_FILE=/config/gcs.json \
|
||||||
|
jkaninda/mysql-bkup backup -d shop --storage gcs --path mysql
|
||||||
|
```
|
||||||
|
|
||||||
|
`STORAGE` (env) and `--storage` are equivalent; `ssh` and `remote` are accepted for `sftp`. The object key is exactly `<path>/<file>`.
|
||||||
|
|
||||||
|
## Encryption
|
||||||
|
|
||||||
|
| Mode | Backup | Restore | File |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GPG passphrase | `GPG_PASSPHRASE` | `GPG_PASSPHRASE` | `.sql.gz.gpg` |
|
||||||
|
| GPG public key | `GPG_PUBLIC_KEY` (path or inline), or `/config/gnupg/public_key.asc` | `GPG_PRIVATE_KEY` (+ `GPG_PRIVATE_KEY_PASSPHRASE`) | `.sql.gz.gpg` |
|
||||||
|
| age recipients | `AGE_RECIPIENTS` (`age1...`, `ssh-ed25519 ...`) or `AGE_RECIPIENTS_FILE` | `AGE_IDENTITY` / `AGE_IDENTITY_FILE` (default `/config/age/identity.txt`) | `.sql.gz.age` |
|
||||||
|
| age passphrase | `AGE_PASSPHRASE` | `AGE_PASSPHRASE` | `.sql.gz.age` |
|
||||||
|
|
||||||
|
`ENCRYPTION=auto` (default) picks the mode from the keys that are set; GPG wins when both GPG and age keys are set. Restore detects the format from the content.
|
||||||
|
|
||||||
|
## Backup index and retention
|
||||||
|
|
||||||
|
Each storage folder holds an index file (`BACKUP_INDEX_NAME`, default `{tool}-metadata.json`; placeholders `{tool}`, `{engine}` (always `mysql`), `{name}`, `{db}`) and one `<backup>.meta.json` sidecar per backup. `list`, `delete`, `prune` and retention work from the index, so mysql-bkup only ever deletes files it created itself, and several instances can share a folder with different index names.
|
||||||
|
|
||||||
|
- `BACKUP_RETENTION_DAYS=N` deletes backups older than N days, `BACKUP_RETENTION_COUNT=N` keeps the newest N. The newest successful backup is always kept.
|
||||||
|
- Backups made before the index existed (1.x) are never deleted, unless `BACKUP_RETENTION_ADOPT_LEGACY=true` imports them.
|
||||||
|
- `BACKUP_INDEX=false` turns the index off; retention then only matches this tool's file names directly in the folder.
|
||||||
|
|
||||||
|
## Multiple databases
|
||||||
|
|
||||||
|
```shell
|
||||||
|
backup -a # every database, one file each (system databases are skipped)
|
||||||
|
backup -a --exclude-db test,tmp
|
||||||
|
backup -A # everything in one file: all_databases_<timestamp>.sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Or a YAML file (`-c /config/config.yaml`, also picked up automatically at `/config/config.yaml`); the 1.x format still works:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cronExpression: "@daily"
|
||||||
|
databases:
|
||||||
|
- name: shop
|
||||||
|
host: mysql1
|
||||||
|
user: backup
|
||||||
|
password: ${SHOP_PASSWORD}
|
||||||
|
path: /shop
|
||||||
|
- name: blog # credentials from DB_USER_BLOG / BLOG_DB_USER ... or the global DB_*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client tools
|
||||||
|
|
||||||
|
| Server | Dump | Restore |
|
||||||
|
|---|---|---|
|
||||||
|
| MariaDB | `mariadb-dump` (MariaDB 11.8 LTS client) | `mariadb` |
|
||||||
|
| MySQL 8.0 / 8.4 / 9.x | Oracle `mysqldump` 8.4 LTS | Oracle `mysql` 8.4 LTS |
|
||||||
|
|
||||||
|
Dumps use `--single-transaction --routines --triggers --events --hex-blob --quick`, plus `--no-tablespaces --set-gtid-purged=OFF` on MySQL. A single-database dump has no `CREATE DATABASE`/`USE`, so it can be restored under another name.
|
||||||
|
|
||||||
|
The image is based on Debian 13 (trixie) slim. Oracle publishes its client for `amd64` and `arm64` only: on `arm/v7` the image has the MariaDB tools only, and MySQL servers are dumped with `mariadb-dump` (a warning is logged). `MYSQL_CLIENT=mysql|mariadb` forces one family.
|
||||||
|
|
||||||
|
TLS: `DB_SSL_MODE=disabled` (default, as in 1.x) `| preferred | required | verify_ca | verify_identity`, with `DB_SSL_CA` (used for the verify modes), `DB_SSL_CERT`, `DB_SSL_KEY`. 1.x values `0` and `1` still mean disabled and required. With the MariaDB client, `verify_ca` also checks the host name.
|
||||||
|
|
||||||
|
Restores are not atomic: MySQL cannot roll back DDL, so a restore that fails halfway leaves the statements before the failure applied (the exit code is non-zero). Restore into a new database, or keep a backup of the target, when that matters.
|
||||||
|
|
||||||
|
## Use with Miabi
|
||||||
|
|
||||||
|
Miabi runs `backup -d <db>`, `backup --storage s3 -d <db> --path <path>` and `restore -d <db> -f <file> [--storage s3 --path <path>]` with `DB_*`, `AWS_*` and `GPG_PASSPHRASE`, and reads the artifact name from the last `file=` in the output. That contract is unchanged and covered by the integration tests.
|
||||||
|
|
||||||
|
## Upgrading from 1.x
|
||||||
|
|
||||||
|
Everything that worked in 1.x keeps working; a few behaviours changed on purpose.
|
||||||
|
|
||||||
|
- **Image:** Debian 13 slim instead of Alpine, with Oracle's `mysqldump` for MySQL servers (correct MySQL 8 dumps) and `mariadb-dump` for MariaDB. `/bin/sh`, `/backup`, `/config`, the `backup`/`restore`/`migrate` shortcuts and the `bkup` alias are still there; `tini` is now PID 1.
|
||||||
|
- **Renamed variables** are still read, with a deprecation warning: `DB_USER` → `DB_USER`, `S3_ENDPOINT` → `AWS_S3_ENDPOINT`, `BUCKET_NAME`/`AWS_BUCKET_NAME` → `AWS_S3_BUCKET_NAME`, `ACCESS_KEY`/`SECRET_KEY` → `AWS_ACCESS_KEY`/`AWS_SECRET_KEY`, `AWS_S3_PATH`/`S3_PATH`/`REMOTE_PATH` → `REMOTE_PATH`, `SFTP_HOST` → `SFTP_HOST`, `SFTP_IDENTITY_FILE` → `SFTP_IDENTITY_FILE`, `FTP_HOST_NAME` → `FTP_HOST`, `BACKUP_TMP_DIR` → `TMP_DIR`.
|
||||||
|
- **`DB_SSL_MODE`** takes MySQL's mode names; `0`/`1` still work.
|
||||||
|
- **`STORAGE` and `STORAGE_PATH` env are honoured** (1.x always used the `--storage` default and `/backup`). Unknown storage names are an error instead of a silent local backup.
|
||||||
|
- **SFTP variables are renamed, and the old names are no longer read:** `SSH_HOST`/`SSH_HOST_NAME` → `SFTP_HOST`, `SSH_PORT` → `SFTP_PORT`, `SSH_USER` → `SFTP_USER`, `SSH_PASSWORD` → `SFTP_PASSWORD`, `SSH_IDENTIFY_FILE`/`SSH_IDENTITY_FILE` → `SFTP_IDENTITY_FILE`, `SSH_REMOTE_PATH` → `REMOTE_PATH`.
|
||||||
|
- **SFTP host keys are verified by default.** Set `SFTP_KNOWN_HOSTS=/path/known_hosts`, or `SFTP_INSECURE_IGNORE_HOST_KEY=true` to keep the 1.x behaviour.
|
||||||
|
- **`MAIL_SKIP_TLS` is read with its documented meaning** (`true` skips certificate verification). 1.x had it inverted, so `MAIL_SKIP_TLS=false` (the value in `.env.example`) turned verification *off*. Use `MAIL_INSECURE_SKIP_VERIFY` from now on.
|
||||||
|
- **Retention only deletes indexed backups** (see above). 1.x deleted every file older than N days in the folder. Set `BACKUP_RETENTION_ADOPT_LEGACY=true` once to let retention handle 1.x backups, or `BACKUP_INDEX=false` for name-based retention.
|
||||||
|
- **Encryption fails closed**: a `GPG_PUBLIC_KEY` that cannot be read is an error (1.x uploaded plaintext). The env var now wins over the auto-detected `/config/gnupg` key.
|
||||||
|
- **Errors no longer kill the daemon**: a failed run is logged, notified and retried at the next schedule; other databases of the same run continue (`BACKUP_RESCUE_MODE` is no longer needed).
|
||||||
|
- **Restore** works from every storage (1.x had no SFTP restore), is case-insensitive about the storage name, and no longer goes through `sh -c`.
|
||||||
|
- **`migrate`** streams directly into the target (no intermediate file), creates the target database if needed, and `TARGET_DB_NAME` defaults to the source name; `-a` migrates every database.
|
||||||
|
- **Notification templates** are built in; custom ones still go in `/config/templates` (`TEMPLATES_DIR`).
|
||||||
|
- **Version** is injected with `-ldflags "-X main.Version=..."` (was `utils.Version`).
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```shell
|
||||||
|
make build # bin/mysql-bkup
|
||||||
|
make test # unit tests
|
||||||
|
make test-integration # Docker: MySQL 8.4/8.0, MariaDB 11.8, MinIO
|
||||||
|
make docker-build VERSION=v2.0.0
|
||||||
|
make env-docs # regenerate the reference below
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
Generated with `mysql-bkup config --env-docs`. Every secret also accepts `<NAME>_FILE`. Resolution order: flag, then env, then deprecated names, then the YAML `env:` section, then the default.
|
||||||
|
|
||||||
|
### storage
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `STORAGE` | `--storage` `-s` | `local` | | Storage backend: local, s3, gcs, azure, sftp (ssh, remote), ftp (local, s3, gcs, azure, sftp, ssh, ftp, remote) |
|
||||||
|
| `STORAGE_PATH` | | `/backup` | | Root directory of the local storage backend |
|
||||||
|
| `REMOTE_PATH` | `--path` `-P` | | `AWS_S3_PATH` (deprecated), `S3_PATH` (deprecated) | Folder inside the storage backend (without file name) |
|
||||||
|
|
||||||
|
### general
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `BACKUP_NAME` | `--name` `-n` | | | Base name of the artifact; a timestamp is appended (default: the database or volume name) |
|
||||||
|
| `BACKUP_CUSTOM_NAME` | `--custom-name` | | | Fixed artifact name without timestamp (overwrites the previous one) |
|
||||||
|
| `FILE_NAME` | `--file` `-f` | | | Backup file to restore, verify or delete |
|
||||||
|
| `BACKUP_CONFIG_FILE` | `--config` `-c` | | | YAML configuration file (default /config/config.yaml if present) |
|
||||||
|
| `BACKUP_TIMEOUT` | | | | Abort a backup after this duration (0 = none) |
|
||||||
|
| `RESTORE_TIMEOUT` | | | | Abort a restore after this duration (0 = none) |
|
||||||
|
| `TMP_DIR` | | `/tmp/backup` | `BACKUP_TMP_DIR` (deprecated) | Scratch directory (only for secret files and disk-based modes) |
|
||||||
|
| `COMPRESSION` | `--compression` | `gzip` | | Compression: gzip, zstd or none (gzip, zstd, none) |
|
||||||
|
| `COMPRESSION_LEVEL` | | | | Compression level (0 = default) |
|
||||||
|
| `DISABLE_COMPRESSION` | `--disable-compression` | | | Same as COMPRESSION=none |
|
||||||
|
| `RESTORE_VERIFY_FIRST` | `--verify-first` | `auto` | | Verify the whole artifact before restoring (auto: on for GPG-encrypted volumes) (auto, true, false) |
|
||||||
|
| `LOG_LEVEL` | | `info` | | Log level (debug, info, warn, error) |
|
||||||
|
| `LOG_FORMAT` | | `text` | | Log format (text, json) |
|
||||||
|
| `TIME_FORMAT` | | `2006-01-02 15:04:05` | | Time layout used in notifications |
|
||||||
|
| `RESULT_FILE` | `--result-file` | | | Write the run result as JSON to this path |
|
||||||
|
|
||||||
|
### scheduling
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `BACKUP_CRON_EXPRESSION` | `--cron-expression` `-e` | | | Run as a daemon on this cron schedule (5 or 6 fields, @daily, @every 6h) |
|
||||||
|
| `BACKUP_RUN_ON_START` | | | | In daemon mode, run one backup immediately at start |
|
||||||
|
| `BACKUP_RESCUE_MODE` | | | | Deprecated: runs always continue with the remaining targets after a failure |
|
||||||
|
| `METRICS_ADDR` | | | | Serve Prometheus /metrics and /healthz on this address in daemon mode (e.g. :9090) |
|
||||||
|
| `HEALTH_FILE` | | `/tmp/bkup-health.json` | | State file read by the health command |
|
||||||
|
| `HEALTH_MAX_AGE` | | | | health fails when the last success is older than this (default: 2x the schedule interval) |
|
||||||
|
|
||||||
|
### retention
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `BACKUP_RETENTION_DAYS` | | `0` | | Delete backups older than N days (0 = keep) |
|
||||||
|
| `BACKUP_RETENTION_COUNT` | | `0` | | Keep only the newest N backups (0 = no limit) |
|
||||||
|
| `BACKUP_RETENTION_ALLOW_ROOT` | | | | Allow pattern-based retention at the storage root (index disabled) |
|
||||||
|
| `BACKUP_RETENTION_ADOPT_LEGACY` | | | | Import pre-index backups matching this tool's naming into the index |
|
||||||
|
| `BACKUP_INDEX` | | `true` | | Keep the backup index file and per-backup .meta.json sidecars |
|
||||||
|
| `BACKUP_INDEX_NAME` | `--index-name` | `{tool}-metadata.json` | | Index file name; placeholders {tool} {engine} {name} {db} |
|
||||||
|
|
||||||
|
### storage-s3
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `AWS_S3_ENDPOINT` | | | `S3_ENDPOINT` (deprecated), `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL` | S3 endpoint (empty = AWS) |
|
||||||
|
| `AWS_S3_BUCKET_NAME` | | | `BUCKET_NAME` (deprecated), `AWS_BUCKET_NAME` (deprecated) | S3 bucket |
|
||||||
|
| `AWS_REGION` | | `us-east-1` | `AWS_DEFAULT_REGION` | S3 region |
|
||||||
|
| `AWS_ACCESS_KEY` | | | `ACCESS_KEY` (deprecated), `AWS_ACCESS_KEY_ID` | S3 access key (empty = default AWS credential chain) |
|
||||||
|
| `AWS_SECRET_KEY` | | | `SECRET_KEY` (deprecated), `AWS_SECRET_ACCESS_KEY`, `AWS_SECRET_KEY_FILE` | S3 secret key |
|
||||||
|
| `AWS_SESSION_TOKEN` | | | `AWS_SESSION_TOKEN_FILE` | S3 session token |
|
||||||
|
| `AWS_PROFILE` | | | | AWS shared config profile |
|
||||||
|
| `AWS_DISABLE_SSL` | | | | Use http for the S3 endpoint |
|
||||||
|
| `AWS_FORCE_PATH_STYLE` | | | | Path-style S3 addressing (MinIO) |
|
||||||
|
| `S3_STORAGE_CLASS` | | | | S3 storage class |
|
||||||
|
| `S3_SSE` | | | | Server-side encryption: AES256 or aws:kms (aes256, aws:kms) |
|
||||||
|
| `S3_SSE_KMS_KEY_ID` | | | | KMS key id for aws:kms |
|
||||||
|
| `S3_PART_SIZE` | | `64MiB` | | Multipart part size; max object size is 10000 x this |
|
||||||
|
| `S3_UPLOAD_CONCURRENCY` | | `2` | | Parallel part uploads (memory = part size x this) |
|
||||||
|
|
||||||
|
### storage-sftp
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `SFTP_HOST` | | | | SFTP host |
|
||||||
|
| `SFTP_PORT` | | `22` | | SFTP port |
|
||||||
|
| `SFTP_USER` | | | | SFTP user |
|
||||||
|
| `SFTP_PASSWORD` | | | `SFTP_PASSWORD_FILE` | SFTP password |
|
||||||
|
| `SFTP_IDENTITY_FILE` | | | | SFTP private key file (the SSH agent is also used when SSH_AUTH_SOCK is set) |
|
||||||
|
| `SFTP_IDENTITY_PASSPHRASE` | | | `SFTP_IDENTITY_PASSPHRASE_FILE` | Passphrase of the SFTP private key |
|
||||||
|
| `SFTP_KNOWN_HOSTS` | | | | known_hosts file used to verify the SFTP host key (default ~/.ssh/known_hosts) |
|
||||||
|
| `SFTP_INSECURE_IGNORE_HOST_KEY` | | | | Do not verify the SFTP host key (insecure) |
|
||||||
|
|
||||||
|
### storage-ftp
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `FTP_HOST` | | | `FTP_HOST_NAME` (deprecated) | FTP host |
|
||||||
|
| `FTP_PORT` | | `21` | | FTP port |
|
||||||
|
| `FTP_USER` | | | | FTP user |
|
||||||
|
| `FTP_PASSWORD` | | | `FTP_PASSWORD_FILE` | FTP password |
|
||||||
|
| `FTP_TLS` | | | | Use explicit FTPS |
|
||||||
|
|
||||||
|
### storage-azure
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `AZURE_STORAGE_ACCOUNT_NAME` | | | | Azure storage account |
|
||||||
|
| `AZURE_STORAGE_ACCOUNT_KEY` | | | `AZURE_STORAGE_ACCOUNT_KEY_FILE` | Azure storage account key |
|
||||||
|
| `AZURE_STORAGE_CONTAINER_NAME` | | | | Azure blob container |
|
||||||
|
| `AZURE_STORAGE_ENDPOINT` | | | | Azure blob endpoint (Azurite, sovereign clouds) |
|
||||||
|
| `AZURE_STORAGE_SAS_TOKEN` | | | `AZURE_STORAGE_SAS_TOKEN_FILE` | Azure SAS token (instead of the account key) |
|
||||||
|
|
||||||
|
### storage-gcs
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `GCS_BUCKET` | | | | GCS bucket |
|
||||||
|
| `GCS_CREDENTIALS_FILE` | | | `GOOGLE_APPLICATION_CREDENTIALS` | GCS service account JSON file |
|
||||||
|
| `GCS_CREDENTIALS_JSON` | | | `GCS_CREDENTIALS_JSON_FILE` | GCS service account JSON content |
|
||||||
|
| `GCS_ENDPOINT` | | | | GCS endpoint (emulators) |
|
||||||
|
|
||||||
|
### encryption
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ENCRYPTION` | `--encryption` | `auto` | | auto (from the keys set), none, gpg or age (auto, none, gpg, age) |
|
||||||
|
| `ENCRYPTION_ARMOR` | | | | ASCII-armor encrypted output |
|
||||||
|
| `GPG_PASSPHRASE` | | | `GPG_PASSPHRASE_FILE` | GPG symmetric passphrase |
|
||||||
|
| `GPG_PUBLIC_KEY` | | | | GPG public key(s): file path or inline key (default /config/gnupg/public_key.{asc,gpg}) |
|
||||||
|
| `GPG_PRIVATE_KEY` | | | `GPG_PRIVATE_KEY_FILE` | GPG private key for restore: file path or inline key (default /config/gnupg/private_key.{asc,gpg}) |
|
||||||
|
| `GPG_PRIVATE_KEY_PASSPHRASE` | | | `GPG_PRIVATE_KEY_PASSPHRASE_FILE` | Passphrase of the GPG private key (default GPG_PASSPHRASE) |
|
||||||
|
| `AGE_RECIPIENTS` | | | | age recipients (age1..., ssh-ed25519 ...), comma or newline separated |
|
||||||
|
| `AGE_RECIPIENTS_FILE` | | | | File with age recipients |
|
||||||
|
| `AGE_PASSPHRASE` | | | `AGE_PASSPHRASE_FILE` | age passphrase (scrypt) |
|
||||||
|
| `AGE_IDENTITY` | | | `AGE_IDENTITY_FILE` | age identity (AGE-SECRET-KEY-...) for restore |
|
||||||
|
| `AGE_IDENTITY_FILE` | | | | age identity file for restore (default /config/age/identity.txt) |
|
||||||
|
|
||||||
|
### notifications
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `MAIL_HOST` | | | | SMTP host |
|
||||||
|
| `MAIL_PORT` | | `587` | | SMTP port |
|
||||||
|
| `MAIL_USERNAME` | | | | SMTP user |
|
||||||
|
| `MAIL_PASSWORD` | | | `MAIL_PASSWORD_FILE` | SMTP password |
|
||||||
|
| `MAIL_FROM` | | | | Sender address |
|
||||||
|
| `MAIL_TO` | | | | Recipients, comma separated |
|
||||||
|
| `MAIL_SKIP_TLS` | | | | Deprecated: true skips certificate verification (use MAIL_INSECURE_SKIP_VERIFY) |
|
||||||
|
| `MAIL_INSECURE_SKIP_VERIFY` | | | | Skip SMTP certificate verification |
|
||||||
|
| `MAIL_TLS` | | `starttls` | | SMTP TLS mode (starttls, tls, none) |
|
||||||
|
| `TG_TOKEN` | | | `TELEGRAM_BOT_TOKEN`, `TG_TOKEN_FILE` | Telegram bot token |
|
||||||
|
| `TG_CHAT_ID` | | | `TELEGRAM_CHAT_ID` | Telegram chat id |
|
||||||
|
| `POSTA_URL` | | | | Posta base URL (https://posta.example.com) for email notifications through Posta |
|
||||||
|
| `POSTA_API_KEY` | | | `POSTA_API_KEY_FILE` | Posta API key with the send scope |
|
||||||
|
| `POSTA_WORKSPACE_ID` | | | | Posta workspace id (only for account-wide API keys) |
|
||||||
|
| `POSTA_FROM` | | | | Posta sender address (default MAIL_FROM) |
|
||||||
|
| `POSTA_TO` | | | | Posta recipients, comma separated (default MAIL_TO) |
|
||||||
|
| `POSTA_TEMPLATE` | | | | Posta template name for notifications; the event fields are passed as template data (default: built-in HTML) |
|
||||||
|
| `POSTA_TEMPLATE_ERROR` | | | | Posta template name for failures (default POSTA_TEMPLATE) |
|
||||||
|
| `POSTA_LANGUAGE` | | | | Posta template language |
|
||||||
|
| `WEBHOOK_URL` | | | | Generic JSON webhook URL |
|
||||||
|
| `WEBHOOK_SECRET` | | | `WEBHOOK_SECRET_FILE` | HMAC-SHA256 secret for the X-Bkup-Signature header |
|
||||||
|
| `SLACK_WEBHOOK_URL` | | | `SLACK_WEBHOOK_URL_FILE` | Slack incoming webhook URL |
|
||||||
|
| `DISCORD_WEBHOOK_URL` | | | `DISCORD_WEBHOOK_URL_FILE` | Discord webhook URL |
|
||||||
|
| `NTFY_URL` | | | | ntfy topic URL |
|
||||||
|
| `NTFY_TOKEN` | | | `NTFY_TOKEN_FILE` | ntfy access token |
|
||||||
|
| `NOTIFY_ON` | | `always` | | When to notify (always, failure, success, never) |
|
||||||
|
| `HEALTHCHECK_PING_URL` | | | | healthchecks.io-style URL pinged on start (/start), success and failure (/fail) |
|
||||||
|
| `BACKUP_REFERENCE` | | | | Free text included in notifications |
|
||||||
|
| `TEMPLATES_DIR` | | `/config/templates` | | Directory with custom notification templates |
|
||||||
|
|
||||||
|
### database
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `DB_URL` | | | `DB_URL_FILE` | Connection URL mysql://user:password@host:port/database?ssl-mode=required (overrides host, port, user, password) |
|
||||||
|
| `DB_HOST` | | `localhost` | | Database host (a path starting with / is a Unix socket) |
|
||||||
|
| `DB_PORT` | | `3306` | | Database port |
|
||||||
|
| `DB_NAME` | `--dbname` `-d` | | | Database name |
|
||||||
|
| `DB_USER` | | | `DB_USERNAME` | Database user |
|
||||||
|
| `DB_PASSWORD` | | | `DB_PASSWORD_FILE` | Database password |
|
||||||
|
| `DB_SSL_MODE` | | `disabled` | | TLS mode: disabled, preferred, required, verify_ca, verify_identity (1.x values 0 and 1 mean disabled and required) |
|
||||||
|
| `DB_SSL_CA` | | `/etc/ssl/certs/ca-certificates.crt` | | CA certificate file, used with DB_SSL_MODE=verify_ca or verify_identity |
|
||||||
|
| `DB_SSL_CERT` | | | | Client certificate file |
|
||||||
|
| `DB_SSL_KEY` | | | | Client key file |
|
||||||
|
| `BACKUP_ALL_DATABASES` | `--all-databases` `-a` | | | Back up every database, one file each |
|
||||||
|
| `BACKUP_ALL_IN_ONE` | `--all-in-one` `-A` | | | Back up every database into a single file |
|
||||||
|
| `BACKUP_EXCLUDE_DATABASES` | `--exclude-db` | | | Databases to skip with --all-databases, comma separated |
|
||||||
|
| `TARGET_DB_URL` | | | `TARGET_DB_URL_FILE` | migrate: target connection URL |
|
||||||
|
| `TARGET_DB_HOST` | | | | migrate: target host |
|
||||||
|
| `TARGET_DB_PORT` | | `3306` | | migrate: target port |
|
||||||
|
| `TARGET_DB_NAME` | | | | migrate: target database (default: the source name) |
|
||||||
|
| `TARGET_DB_USER` | | | `TARGET_DB_USERNAME` | migrate: target user |
|
||||||
|
| `TARGET_DB_PASSWORD` | | | `TARGET_DB_PASSWORD_FILE` | migrate: target password |
|
||||||
|
| `TARGET_DB_SSL_MODE` | | `disabled` | | migrate: target TLS mode (same values as DB_SSL_MODE) |
|
||||||
|
| `TARGET_DB_SSL_CA` | | `/etc/ssl/certs/ca-certificates.crt` | | migrate: target CA certificate file |
|
||||||
|
| `MYSQL_CLIENT` | | `auto` | | Client tools: auto (mariadb-dump for MariaDB, Oracle mysqldump for MySQL), mysql or mariadb (auto, mysql, mariadb) |
|
||||||
|
|
||||||
|
|
||||||
## Available image registries
|
## Available image registries
|
||||||
|
|
||||||
This Docker image is published to both Docker Hub and the GitHub container registry.
|
|
||||||
Depending on your preferences and needs, you can reference both `jkaninda/mysql-bkup` as well as `ghcr.io/jkaninda/mysql-bkup`:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
docker pull jkaninda/mysql-bkup:v1.0
|
docker pull jkaninda/mysql-bkup
|
||||||
docker pull ghcr.io/jkaninda/mysql-bkup:v1.0
|
docker pull ghcr.io/jkaninda/mysql-bkup
|
||||||
```
|
```
|
||||||
|
|
||||||
Documentation references Docker Hub, but all examples will work using ghcr.io just as well.
|
Tags: `latest`, and immutable release tags (`v2.0.0`, `2.0.0`, `2.0`, `2`). Pin a release tag in production.
|
||||||
|
|
||||||
## Supported Engines
|
## PostgreSQL
|
||||||
|
|
||||||
This image is developed and tested against the Docker CE engine and Kubernetes exclusively.
|
|
||||||
While it may work against different implementations, there are no guarantees about support for non-Docker engines.
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
|
|
||||||
|
|
||||||
- The original image is based on `ubuntu` and requires additional tools, making it heavy.
|
|
||||||
- This image is written in Go.
|
|
||||||
- `arm64` and `arm/v7` architectures are supported.
|
|
||||||
- Docker in Swarm mode is supported.
|
|
||||||
- Kubernetes is supported.
|
|
||||||
|
|
||||||
|
Looking for PostgreSQL? See [pg-bkup](https://github.com/jkaninda/pg-bkup).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License. See the LICENSE file for details.
|
This project is licensed under the MIT License. See the LICENSE file for details.
|
||||||
|
|
||||||
## Authors
|
|
||||||
|
|
||||||
**Jonas Kaninda**
|
|
||||||
- <https://github.com/jkaninda>
|
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jkaninda/mysql-bkup/pkg"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var BackupCmd = &cobra.Command{
|
|
||||||
Use: "backup ",
|
|
||||||
Short: "Backup database operation",
|
|
||||||
Example: utils.BackupExample,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
pkg.StartBackup(cmd)
|
|
||||||
} else {
|
|
||||||
utils.Fatal("Error, no argument required")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
//Backup
|
|
||||||
BackupCmd.PersistentFlags().StringP("mode", "m", "default", "Execution mode. default or scheduled")
|
|
||||||
BackupCmd.PersistentFlags().StringP("period", "", "0 1 * * *", "Schedule period time")
|
|
||||||
BackupCmd.PersistentFlags().BoolP("prune", "", false, "Delete old backup, default disabled")
|
|
||||||
BackupCmd.PersistentFlags().IntP("keep-last", "", 7, "Delete files created more than specified days ago, default 7 days")
|
|
||||||
BackupCmd.PersistentFlags().BoolP("disable-compression", "", false, "Disable backup compression")
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jkaninda/mysql-bkup/pkg"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var RestoreCmd = &cobra.Command{
|
|
||||||
Use: "restore",
|
|
||||||
Short: "Restore database operation",
|
|
||||||
Example: utils.RestoreExample,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
pkg.StartRestore(cmd)
|
|
||||||
} else {
|
|
||||||
utils.Fatal("Error, no argument required")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
//Restore
|
|
||||||
RestoreCmd.PersistentFlags().StringP("file", "f", "", "File name of database")
|
|
||||||
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
// Package cmd /*
|
|
||||||
/*
|
|
||||||
Copyright © 2024 Jonas Kaninda
|
|
||||||
*/
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// rootCmd represents the base command when called without any subcommands
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "mysql-bkup [Command]",
|
|
||||||
Short: "MySQL Backup tool, backup database to S3 or Object Storage",
|
|
||||||
Long: `MySQL Database backup and restoration tool. Backup database to AWS S3 storage or any S3 Alternatives for Object Storage.`,
|
|
||||||
Example: utils.MainExample,
|
|
||||||
Version: appVersion,
|
|
||||||
}
|
|
||||||
var operation = ""
|
|
||||||
|
|
||||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
|
||||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
|
||||||
func Execute() {
|
|
||||||
err := rootCmd.Execute()
|
|
||||||
if err != nil {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.PersistentFlags().StringP("storage", "s", "local", "Storage. local or s3")
|
|
||||||
rootCmd.PersistentFlags().StringP("path", "P", "", "AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup`")
|
|
||||||
rootCmd.PersistentFlags().StringP("dbname", "d", "", "Database name")
|
|
||||||
rootCmd.PersistentFlags().IntP("port", "p", 3306, "Database port")
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&operation, "operation", "o", "", "Set operation, for old version only")
|
|
||||||
|
|
||||||
rootCmd.AddCommand(VersionCmd)
|
|
||||||
rootCmd.AddCommand(BackupCmd)
|
|
||||||
rootCmd.AddCommand(RestoreCmd)
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
/*
|
|
||||||
Copyright © 2024 Jonas Kaninda
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var appVersion = os.Getenv("VERSION")
|
|
||||||
|
|
||||||
var VersionCmd = &cobra.Command{
|
|
||||||
Use: "version",
|
|
||||||
Short: "Show version",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
Version()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func Version() {
|
|
||||||
fmt.Printf("Version: %s \n", appVersion)
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
FROM golang:1.22.5 AS build
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy the source code.
|
|
||||||
COPY . .
|
|
||||||
# Installs Go dependencies
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Build
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/mysql-bkup
|
|
||||||
|
|
||||||
FROM ubuntu:24.04
|
|
||||||
ENV DB_HOST=""
|
|
||||||
ENV DB_NAME=""
|
|
||||||
ENV DB_USERNAME=""
|
|
||||||
ENV DB_PASSWORD=""
|
|
||||||
ENV DB_PORT="3306"
|
|
||||||
ENV STORAGE=local
|
|
||||||
ENV AWS_S3_ENDPOINT=""
|
|
||||||
ENV AWS_S3_BUCKET_NAME=""
|
|
||||||
ENV AWS_ACCESS_KEY=""
|
|
||||||
ENV AWS_SECRET_KEY=""
|
|
||||||
ENV AWS_REGION="us-west-2"
|
|
||||||
ENV AWS_S3_PATH=""
|
|
||||||
ENV AWS_DISABLE_SSL="false"
|
|
||||||
ENV GPG_PASSPHRASE=""
|
|
||||||
ENV SSH_USER=""
|
|
||||||
ENV SSH_REMOTE_PATH=""
|
|
||||||
ENV SSH_PASSWORD=""
|
|
||||||
ENV SSH_HOST_NAME=""
|
|
||||||
ENV SSH_IDENTIFY_FILE=""
|
|
||||||
ENV SSH_PORT="22"
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
ENV VERSION="v1.0"
|
|
||||||
ARG WORKDIR="/app"
|
|
||||||
ARG BACKUPDIR="/backup"
|
|
||||||
ARG BACKUP_TMP_DIR="/tmp/backup"
|
|
||||||
ARG BACKUP_CRON="/etc/cron.d/backup_cron"
|
|
||||||
ARG BACKUP_CRON_SCRIPT="/usr/local/bin/backup_cron.sh"
|
|
||||||
LABEL author="Jonas Kaninda"
|
|
||||||
|
|
||||||
RUN apt-get update -qq
|
|
||||||
#RUN apt-get install build-essential libcurl4-openssl-dev libxml2-dev mime-support -y
|
|
||||||
RUN apt install mysql-client supervisor cron gnupg -y
|
|
||||||
|
|
||||||
# Clear cache
|
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
RUN mkdir $WORKDIR
|
|
||||||
RUN mkdir $BACKUPDIR
|
|
||||||
RUN mkdir -p $BACKUP_TMP_DIR
|
|
||||||
RUN chmod 777 $WORKDIR
|
|
||||||
RUN chmod 777 $BACKUPDIR
|
|
||||||
RUN chmod 777 $BACKUP_TMP_DIR
|
|
||||||
RUN touch $BACKUP_CRON && \
|
|
||||||
touch $BACKUP_CRON_SCRIPT && \
|
|
||||||
chmod 777 $BACKUP_CRON && \
|
|
||||||
chmod 777 $BACKUP_CRON_SCRIPT
|
|
||||||
|
|
||||||
COPY --from=build /app/mysql-bkup /usr/local/bin/mysql-bkup
|
|
||||||
RUN chmod +x /usr/local/bin/mysql-bkup
|
|
||||||
|
|
||||||
RUN ln -s /usr/local/bin/mysql-bkup /usr/local/bin/bkup
|
|
||||||
|
|
||||||
ADD docker/supervisord.conf /etc/supervisor/supervisord.conf
|
|
||||||
|
|
||||||
WORKDIR $WORKDIR
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
[supervisord]
|
|
||||||
nodaemon=true
|
|
||||||
user=root
|
|
||||||
logfile=/var/log/supervisor/supervisord.log
|
|
||||||
pidfile=/var/run/supervisord.pid
|
|
||||||
|
|
||||||
[program:cron]
|
|
||||||
command = /bin/bash -c "declare -p | grep -Ev '^declare -[[:alpha:]]*r' > /run/supervisord.env && /usr/sbin/cron -f -L 15"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
user = root
|
|
||||||
stderr_logfile=/var/log/cron.err.log
|
|
||||||
stdout_logfile=/var/log/cron.out.log
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
FROM ruby:3.3.4
|
|
||||||
|
|
||||||
ENV LC_ALL C.UTF-8
|
|
||||||
ENV LANG en_US.UTF-8
|
|
||||||
ENV LANGUAGE en_US.UTF-8
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY . ./
|
|
||||||
RUN gem install bundler && bundle install
|
|
||||||
|
|
||||||
EXPOSE 4000
|
|
||||||
+4
-3
@@ -13,13 +13,14 @@
|
|||||||
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
|
||||||
# You can create any custom variable you would like, and they will be accessible
|
# You can create any custom variable you would like, and they will be accessible
|
||||||
# in the templates via {{ site.myvariable }}.
|
# in the templates via {{ site.myvariable }}.
|
||||||
title: MySQL database backup
|
title: MySQL Backup Docker container image
|
||||||
email: hi@jonaskaninda.com
|
email: hi@jonaskaninda.com
|
||||||
description: >- # this means to ignore newlines until "baseurl:"
|
description: >- # this means to ignore newlines until "baseurl:"
|
||||||
MySQL Backup and Restore Docker container image. Backup database to AWS S3 storage or SSH remote server.
|
MySQL Backup is a Docker container image that can be used to backup and restore MySQL database.
|
||||||
|
It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, and SSH compatible storage.
|
||||||
|
|
||||||
baseurl: "" # the subpath of your site, e.g. /blog
|
baseurl: "" # the subpath of your site, e.g. /blog
|
||||||
url: "jkaninda.github.io/mysql-bkup/" # the base hostname & protocol for your site, e.g. http://example.com
|
url: "" # the base hostname & protocol for your site, e.g. http://example.com
|
||||||
twitter_username: jonaskaninda
|
twitter_username: jonaskaninda
|
||||||
github_username: jkaninda
|
github_username: jkaninda
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
services:
|
|
||||||
jekyll:
|
|
||||||
build:
|
|
||||||
context: ./
|
|
||||||
ports:
|
|
||||||
- 4000:4000
|
|
||||||
environment:
|
|
||||||
- JEKYLL_ENV=development
|
|
||||||
volumes:
|
|
||||||
- .:/usr/src/app
|
|
||||||
stdin_open: true
|
|
||||||
tty: true
|
|
||||||
command: bundle exec jekyll serve -H 0.0.0.0 -t
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
title: Azure Blob storage
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backup to Azure Blob Storage
|
||||||
|
|
||||||
|
To store your backups on Azure Blob Storage, you can configure the backup process to use the `--storage azure` option.
|
||||||
|
|
||||||
|
This section explains how to set up and configure Azure Blob-based backups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Steps
|
||||||
|
|
||||||
|
1. **Specify the Storage Type**
|
||||||
|
Add the `--storage azure` flag to your backup command.
|
||||||
|
|
||||||
|
2. **Set the Blob Path**
|
||||||
|
Optionally, specify a custom folder within your Azure Blob container where backups will be stored using the `--path` flag.
|
||||||
|
Example: `--path my-custom-path`.
|
||||||
|
|
||||||
|
3. **Required Environment Variables**
|
||||||
|
The following environment variables are mandatory for Azure Blob-based backups:
|
||||||
|
|
||||||
|
- `AZURE_STORAGE_CONTAINER_NAME`: The name of the Azure Blob container where backups will be stored.
|
||||||
|
- `AZURE_STORAGE_ACCOUNT_NAME`: The name of your Azure Storage account.
|
||||||
|
- `AZURE_STORAGE_ACCOUNT_KEY`: The access key for your Azure Storage account.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for backing up to Azure Blob Storage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysqlbkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup --storage azure -d database --path my-custom-path
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## Azure Blob Configuration
|
||||||
|
- AZURE_STORAGE_CONTAINER_NAME=backup-container
|
||||||
|
- AZURE_STORAGE_ACCOUNT_NAME=account-name
|
||||||
|
- AZURE_STORAGE_ACCOUNT_KEY=Ppby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||||
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Custom Path**: Use the `--path` flag to specify a folder within your Azure Blob container for organizing backups.
|
||||||
|
- **Security**: Ensure your `AZURE_STORAGE_ACCOUNT_KEY` is kept secure and not exposed in public repositories.
|
||||||
|
- **Compatibility**: This configuration works with Azure Blob Storage and other compatible storage solutions.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
title: Backup all databases in the server
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 12
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backup All Databases
|
||||||
|
|
||||||
|
MySQL-Bkup supports backing up all databases on the server using the `--all-databases` (`-a`) flag. By default, this creates separate backup files for each database. If you prefer a single backup file, you can use the `--all-in-one` (`-A`) flag.
|
||||||
|
|
||||||
|
Backing up all databases is useful for creating a snapshot of the entire database server, whether for disaster recovery or migration purposes.
|
||||||
|
## Backup Modes
|
||||||
|
|
||||||
|
### Separate Backup Files (Default)
|
||||||
|
|
||||||
|
Using --all-databases without --all-in-one creates individual backup files for each database.
|
||||||
|
|
||||||
|
- Creates separate backup files for each database.
|
||||||
|
- Provides more flexibility in restoring individual databases or tables.
|
||||||
|
- Can be more manageable in cases where different databases have different retention policies.
|
||||||
|
- Might take slightly longer due to multiple file operations.
|
||||||
|
- It is the default behavior when using the `--all-databases` flag.
|
||||||
|
- It does not backup system databases (`information_schema`, `performance_schema`, `mysql`, `sys`, `innodb`,...).
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
-e "DB_HOST=dbhost" \
|
||||||
|
-e "DB_PORT=3306" \
|
||||||
|
-e "DB_USER=username" \
|
||||||
|
-e "DB_PASSWORD=password" \
|
||||||
|
jkaninda/mysql-bkup backup --all-databases
|
||||||
|
```
|
||||||
|
### Single Backup File
|
||||||
|
|
||||||
|
Using --all-in-one (-A) creates a single backup file containing all databases.
|
||||||
|
|
||||||
|
- Creates a single backup file containing all databases.
|
||||||
|
- Easier to manage if you need to restore everything at once.
|
||||||
|
- Faster to back up and restore in bulk.
|
||||||
|
- Can be problematic if you only need to restore a specific database or table.
|
||||||
|
- It is recommended to use this option for disaster recovery purposes.
|
||||||
|
- It backups system databases as well.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
-e "DB_HOST=dbhost" \
|
||||||
|
-e "DB_PORT=3306" \
|
||||||
|
-e "DB_USER=username" \
|
||||||
|
-e "DB_PASSWORD=password" \
|
||||||
|
jkaninda/mysql-bkup backup --all-in-one
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Which?
|
||||||
|
|
||||||
|
- Use `--all-in-one` if you want a quick, simple backup for disaster recovery where you'll restore everything at once.
|
||||||
|
- Use `--all-databases` if you need granularity in restoring specific databases or tables without affecting others.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Backup to FTP remote server
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 4
|
||||||
|
---
|
||||||
|
|
||||||
|
# Backup to FTP Remote Server
|
||||||
|
|
||||||
|
To store your backups on an FTP remote server, you can configure the backup process to use the `--storage ftp` option.
|
||||||
|
|
||||||
|
This section explains how to set up and configure FTP-based backups.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Steps
|
||||||
|
|
||||||
|
1. **Specify the Storage Type**
|
||||||
|
Add the `--storage ftp` flag to your backup command.
|
||||||
|
|
||||||
|
2. **Set the Remote Path**
|
||||||
|
Define the full remote path where backups will be stored using the `--path` flag or the `REMOTE_PATH` environment variable.
|
||||||
|
Example: `--path /home/jkaninda/backups`.
|
||||||
|
|
||||||
|
3. **Required Environment Variables**
|
||||||
|
The following environment variables are mandatory for FTP-based backups:
|
||||||
|
|
||||||
|
- `FTP_HOST`: The hostname or IP address of the FTP server.
|
||||||
|
- `FTP_PORT`: The FTP port (default is `21`).
|
||||||
|
- `FTP_USER`: The username for FTP authentication.
|
||||||
|
- `FTP_PASSWORD`: The password for FTP authentication.
|
||||||
|
- `REMOTE_PATH`: The directory on the FTP server where backups will be stored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for backing up to an FTP remote server:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup --storage ftp -d database
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## FTP Configuration
|
||||||
|
- FTP_HOST=hostname
|
||||||
|
- FTP_PORT=21
|
||||||
|
- FTP_USER=user
|
||||||
|
- FTP_PASSWORD=password
|
||||||
|
- REMOTE_PATH=/home/jkaninda/backups
|
||||||
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Security**: FTP transmits data, including passwords, in plaintext. For better security, consider using SFTP (SSH File Transfer Protocol) or FTPS (FTP Secure) if supported by your server.
|
||||||
|
- **Remote Path**: Ensure the `REMOTE_PATH` directory exists on the FTP server and is writable by the specified `FTP_USER`.
|
||||||
+105
-118
@@ -6,134 +6,121 @@ nav_order: 2
|
|||||||
---
|
---
|
||||||
# Backup to AWS S3
|
# Backup to AWS S3
|
||||||
|
|
||||||
{: .note }
|
To store your backups on AWS S3, you can configure the backup process to use the `--storage s3` option. This section explains how to set up and configure S3-based backups.
|
||||||
As described on local backup section, to change the storage of you backup and use S3 as storage. You need to add `--storage s3` (-s s3).
|
|
||||||
You can also specify a specify folder where you want to save you data by adding `--path /my-custom-path` flag.
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Backup to S3
|
## Configuration Steps
|
||||||
|
|
||||||
```yml
|
1. **Specify the Storage Type**
|
||||||
services:
|
Add the `--storage s3` flag to your backup command.
|
||||||
mysql-bkup:
|
|
||||||
# In production, it is advised to lock your image tag to a proper
|
|
||||||
# release version instead of using `latest`.
|
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
|
||||||
container_name: mysql-bkup
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --storage s3 -d database --path /my-custom-path
|
|
||||||
environment:
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_HOST=mysql
|
|
||||||
- DB_NAME=database
|
|
||||||
- DB_USERNAME=username
|
|
||||||
- DB_PASSWORD=password
|
|
||||||
## AWS configurations
|
|
||||||
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
|
||||||
- AWS_S3_BUCKET_NAME=backup
|
|
||||||
- AWS_REGION="us-west-2"
|
|
||||||
- AWS_ACCESS_KEY=xxxx
|
|
||||||
- AWS_SECRET_KEY=xxxxx
|
|
||||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
|
||||||
- AWS_DISABLE_SSL="false"
|
|
||||||
|
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
2. **Set the S3 Path**
|
||||||
networks:
|
Optionally, specify a custom folder within your S3 bucket where backups will be stored using the `--path` flag.
|
||||||
- web
|
Example: `--path /my-custom-path`.
|
||||||
networks:
|
|
||||||
web:
|
|
||||||
```
|
|
||||||
|
|
||||||
### Recurring backups to S3
|
3. **Required Environment Variables**
|
||||||
|
The following environment variables are mandatory for S3-based backups:
|
||||||
|
|
||||||
As explained above, you need just to add AWS environment variables and specify the storage type `--storage s3`.
|
- `AWS_S3_ENDPOINT`: The S3 endpoint URL (e.g., `https://s3.amazonaws.com`).
|
||||||
In case you need to use recurring backups, you can use `--mode scheduled` and specify the periodical backup time by adding `--period "0 1 * * *"` flag as described below.
|
- `AWS_S3_BUCKET_NAME`: The name of the S3 bucket where backups will be stored.
|
||||||
|
- `AWS_REGION`: The AWS region where the bucket is located (e.g., `us-west-2`).
|
||||||
|
- `AWS_ACCESS_KEY`: Your AWS access key.
|
||||||
|
- `AWS_SECRET_KEY`: Your AWS secret key.
|
||||||
|
- `AWS_DISABLE_SSL`: Set to `"true"` if using an S3 alternative like Minio without SSL (default is `"false"`).
|
||||||
|
- `AWS_FORCE_PATH_STYLE`: Set to `"true"` if using an S3 alternative like Minio (default is `"false"`).
|
||||||
|
|
||||||
```yml
|
---
|
||||||
services:
|
|
||||||
mysql-bkup:
|
|
||||||
# In production, it is advised to lock your image tag to a proper
|
|
||||||
# release version instead of using `latest`.
|
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
|
||||||
container_name: mysql-bkup
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --storage s3 -d my-database --mode scheduled --period "0 1 * * *"
|
|
||||||
environment:
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_HOST=mysql
|
|
||||||
- DB_NAME=database
|
|
||||||
- DB_USERNAME=username
|
|
||||||
- DB_PASSWORD=password
|
|
||||||
## AWS configurations
|
|
||||||
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
|
||||||
- AWS_S3_BUCKET_NAME=backup
|
|
||||||
- AWS_REGION="us-west-2"
|
|
||||||
- AWS_ACCESS_KEY=xxxx
|
|
||||||
- AWS_SECRET_KEY=xxxxx
|
|
||||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
|
||||||
- AWS_DISABLE_SSL="false"
|
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
networks:
|
|
||||||
web:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deploy on Kubernetes
|
## Example Configuration
|
||||||
|
|
||||||
For Kubernetes, you don't need to run it in scheduled mode. You can deploy it as CronJob.
|
Below is an example `docker-compose.yml` configuration for backing up to AWS S3:
|
||||||
|
|
||||||
### Simple Kubernetes CronJob usage:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: batch/v1
|
services:
|
||||||
kind: CronJob
|
mysql-bkup:
|
||||||
metadata:
|
# In production, lock your image tag to a specific release version
|
||||||
name: bkup-job
|
# instead of using `latest`. Check https://github.com/jkaninda/pg-bkup/releases
|
||||||
spec:
|
# for available releases.
|
||||||
schedule: "0 1 * * *"
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: mysql-bkup
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
command:
|
container_name: pg-bkup
|
||||||
- /bin/sh
|
command: backup --storage s3 -d database --path /my-custom-path
|
||||||
- -c
|
environment:
|
||||||
- mysql-bkup backup -s s3 --path /custom_path
|
- DB_PORT=5432
|
||||||
env:
|
- DB_HOST=postgres
|
||||||
- name: DB_PORT
|
- DB_NAME=database
|
||||||
value: "3306"
|
- DB_USER=username
|
||||||
- name: DB_HOST
|
- DB_PASSWORD=password
|
||||||
value: ""
|
## AWS Configuration
|
||||||
- name: DB_NAME
|
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
value: ""
|
- AWS_S3_BUCKET_NAME=backup
|
||||||
- name: DB_USERNAME
|
- AWS_REGION=us-west-2
|
||||||
value: ""
|
- AWS_ACCESS_KEY=xxxx
|
||||||
# Please use secret!
|
- AWS_SECRET_KEY=xxxxx
|
||||||
- name: DB_PASSWORD
|
## Optional: Disable SSL for S3 alternatives like Minio
|
||||||
value: ""
|
- AWS_DISABLE_SSL=false
|
||||||
- name: AWS_S3_ENDPOINT
|
## Optional: Enable path-style access for S3 alternatives like Minio
|
||||||
value: "https://s3.amazonaws.com"
|
- AWS_FORCE_PATH_STYLE=false
|
||||||
- name: AWS_S3_BUCKET_NAME
|
|
||||||
value: "xxx"
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
- name: AWS_REGION
|
networks:
|
||||||
value: "us-west-2"
|
- web
|
||||||
- name: AWS_ACCESS_KEY
|
|
||||||
value: "xxxx"
|
networks:
|
||||||
- name: AWS_SECRET_KEY
|
web:
|
||||||
value: "xxxx"
|
|
||||||
- name: AWS_DISABLE_SSL
|
|
||||||
value: "false"
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recurring Backups to S3
|
||||||
|
|
||||||
|
To schedule recurring backups to S3, use the `--cron-expression` flag or the `BACKUP_CRON_EXPRESSION` environment variable. This allows you to define a cron schedule for automated backups.
|
||||||
|
|
||||||
|
### Example: Recurring Backup Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup --storage s3 -d database --cron-expression "0 1 * * *"
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## AWS Configuration
|
||||||
|
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
|
- AWS_S3_BUCKET_NAME=backup
|
||||||
|
- AWS_REGION=us-west-2
|
||||||
|
- AWS_ACCESS_KEY=xxxx
|
||||||
|
- AWS_SECRET_KEY=xxxxx
|
||||||
|
## Optional: Define a cron schedule for recurring backups
|
||||||
|
#- BACKUP_CRON_EXPRESSION=0 1 * * *
|
||||||
|
## Optional: Delete old backups after a specified number of days
|
||||||
|
#- BACKUP_RETENTION_DAYS=7
|
||||||
|
## Optional: Disable SSL for S3 alternatives like Minio
|
||||||
|
- AWS_DISABLE_SSL=false
|
||||||
|
## Optional: Enable path-style access for S3 alternatives like Minio
|
||||||
|
- AWS_FORCE_PATH_STYLE=false
|
||||||
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Cron Expression**: Use the `--cron-expression` flag or `BACKUP_CRON_EXPRESSION` environment variable to define the backup schedule. For example, `0 1 * * *` runs the backup daily at 1:00 AM.
|
||||||
|
- **Backup Retention**: Optionally, use the `BACKUP_RETENTION_DAYS` environment variable to automatically delete backups older than a specified number of days.
|
||||||
|
- **S3 Alternatives**: If using an S3 alternative like Minio, set `AWS_DISABLE_SSL=true` and `AWS_FORCE_PATH_STYLE=true` as needed. In the Compose list syntax, do not quote values: the quotes would become part of the value.
|
||||||
|
|
||||||
+101
-115
@@ -1,146 +1,132 @@
|
|||||||
---
|
---
|
||||||
title: Backup to SSH
|
title: Backup to SSH or SFTP
|
||||||
layout: default
|
layout: default
|
||||||
parent: How Tos
|
parent: How Tos
|
||||||
nav_order: 3
|
nav_order: 3
|
||||||
---
|
---
|
||||||
# Backup to SSH remote server
|
# Backup to SFTP or SSH Remote Server
|
||||||
|
|
||||||
|
To store your backups on an `SFTP` or `SSH` remote server instead of the default storage, you can configure the backup process to use the `--storage ssh` or `--storage remote` option.
|
||||||
|
This section explains how to set up and configure SSH-based backups.
|
||||||
|
|
||||||
As described for s3 backup section, to change the storage of you backup and use S3 as storage. You need to add `--storage ssh` or `--storage remote`.
|
---
|
||||||
You need to add the full remote path by adding `--path /home/jkaninda/backups` flag or using `SSH_REMOTE_PATH` environment variable.
|
|
||||||
|
|
||||||
{: .note }
|
## Configuration Steps
|
||||||
These environment variables are required for SSH backup `SSH_HOST_NAME`, `SSH_USER`, `SSH_REMOTE_PATH`, `SSH_IDENTIFY_FILE`, `SSH_PORT` or `SSH_PASSWORD` if you dont use a private key to access to your server.
|
|
||||||
Accessing the remote server using password is not recommended, use private key instead.
|
|
||||||
|
|
||||||
```yml
|
1. **Specify the Storage Type**
|
||||||
|
Add the `--storage ssh` or `--storage remote` flag to your backup command.
|
||||||
|
|
||||||
|
2. **Set the Remote Path**
|
||||||
|
Define the full remote path where backups will be stored using the `--path` flag or the `REMOTE_PATH` environment variable.
|
||||||
|
Example: `--path /home/jkaninda/backups`.
|
||||||
|
|
||||||
|
3. **Required Environment Variables**
|
||||||
|
The following environment variables are mandatory for SSH-based backups:
|
||||||
|
|
||||||
|
- `SFTP_HOST`: The hostname or IP address of the remote server.
|
||||||
|
- `SFTP_USER`: The username for SSH authentication.
|
||||||
|
- `REMOTE_PATH`: The directory on the remote server where backups will be stored.
|
||||||
|
- `SFTP_IDENTITY_FILE`: The path to the private key file for SSH authentication (`SFTP_IDENTITY_FILE` still works).
|
||||||
|
- `SFTP_KNOWN_HOSTS`: A `known_hosts` file used to verify the server's host key. Host keys are verified by default since v2; `SFTP_INSECURE_IGNORE_HOST_KEY=true` turns the check off (not recommended).
|
||||||
|
- `SFTP_PORT`: The SSH port (default is `22`).
|
||||||
|
- `SFTP_PASSWORD`: (Optional) Use this only if you are not using a private key for authentication.
|
||||||
|
|
||||||
|
{: .note }
|
||||||
|
**Security Recommendation**: Using a private key (`SFTP_IDENTITY_FILE`) is strongly recommended over password-based authentication (`SFTP_PASSWORD`) for better security.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for backing up to an SSH remote server:
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup --storage remote -d database
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --storage remote -d database
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./id_ed25519:/tmp/id_ed25519"
|
- ./id_ed25519:/tmp/id_ed25519
|
||||||
environment:
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_HOST=mysql
|
|
||||||
#- DB_NAME=database
|
|
||||||
- DB_USERNAME=username
|
|
||||||
- DB_PASSWORD=password
|
|
||||||
## SSH config
|
|
||||||
- SSH_HOST_NAME="hostname"
|
|
||||||
- SSH_PORT=22
|
|
||||||
- SSH_USER=user
|
|
||||||
- SSH_REMOTE_PATH=/home/jkaninda/backups
|
|
||||||
- SSH_IDENTIFY_FILE=/tmp/id_ed25519
|
|
||||||
## We advise you to use a private jey instead of password
|
|
||||||
#- SSH_PASSWORD=password
|
|
||||||
|
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
networks:
|
|
||||||
web:
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Recurring backups to SSH remote server
|
|
||||||
|
|
||||||
As explained above, you need just to add required environment variables and specify the storage type `--storage ssh`.
|
|
||||||
You can use `--mode scheduled` and specify the periodical backup time by adding `--period "0 1 * * *"` flag as described below.
|
|
||||||
|
|
||||||
```yml
|
|
||||||
services:
|
|
||||||
mysql-bkup:
|
|
||||||
# In production, it is advised to lock your image tag to a proper
|
|
||||||
# release version instead of using `latest`.
|
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
|
||||||
container_name: mysql-bkup
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup -d database --storage ssh --mode scheduled --period "0 1 * * *"
|
|
||||||
volumes:
|
|
||||||
- ./id_ed25519:/tmp/id_ed25519"
|
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
## SSH config
|
## SSH Configuration
|
||||||
- SSH_HOST_NAME="hostname"
|
- SFTP_HOST=hostname
|
||||||
- SSH_PORT=22
|
- SFTP_PORT=22
|
||||||
- SSH_USER=user
|
- SFTP_USER=user
|
||||||
- SSH_REMOTE_PATH=/home/jkaninda/backups
|
- REMOTE_PATH=/home/jkaninda/backups
|
||||||
- SSH_IDENTIFY_FILE=/tmp/id_ed25519
|
- SFTP_IDENTITY_FILE=/tmp/id_ed25519
|
||||||
## We advise you to use a private jey instead of password
|
- SFTP_KNOWN_HOSTS=/config/known_hosts
|
||||||
#- SSH_PASSWORD=password
|
## Optional: Use password instead of private key (not recommended)
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
#- SFTP_PASSWORD=password
|
||||||
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploy on Kubernetes
|
---
|
||||||
|
|
||||||
For Kubernetes, you don't need to run it in scheduled mode.
|
## Recurring Backups to SSH Remote Server
|
||||||
You can deploy it as CronJob.
|
|
||||||
|
|
||||||
Simple Kubernetes CronJob usage:
|
To schedule recurring backups, you can use the `--cron-expression` flag or the `BACKUP_CRON_EXPRESSION` environment variable.
|
||||||
|
This allows you to define a cron schedule for automated backups.
|
||||||
|
|
||||||
|
### Example: Recurring Backup Configuration
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: batch/v1
|
services:
|
||||||
kind: CronJob
|
mysql-bkup:
|
||||||
metadata:
|
# In production, lock your image tag to a specific release version
|
||||||
name: bkup-job
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
spec:
|
# for available releases.
|
||||||
schedule: "0 1 * * *"
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: mysql-bkup
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
command:
|
container_name: mysql-bkup
|
||||||
- /bin/sh
|
command: backup -d database --storage ssh --cron-expression "@daily"
|
||||||
- -c
|
volumes:
|
||||||
- mysql-bkup backup -s ssh
|
- ./id_ed25519:/tmp/id_ed25519
|
||||||
env:
|
environment:
|
||||||
- name: DB_PORT
|
- DB_PORT=3306
|
||||||
value: "3306"
|
- DB_HOST=postgres
|
||||||
- name: DB_HOST
|
- DB_NAME=database
|
||||||
value: ""
|
- DB_USER=username
|
||||||
- name: DB_NAME
|
- DB_PASSWORD=password
|
||||||
value: ""
|
## SSH Configuration
|
||||||
- name: DB_USERNAME
|
- SFTP_HOST=hostname
|
||||||
value: ""
|
- SFTP_PORT=22
|
||||||
# Please use secret!
|
- SFTP_USER=user
|
||||||
- name: DB_PASSWORD
|
- REMOTE_PATH=/home/jkaninda/backups
|
||||||
value: ""
|
- SFTP_IDENTITY_FILE=/tmp/id_ed25519
|
||||||
- name: SSH_HOST_NAME
|
- SFTP_KNOWN_HOSTS=/config/known_hosts
|
||||||
value: ""
|
## Optional: Delete old backups after a specified number of days
|
||||||
- name: SSH_PORT
|
#- BACKUP_RETENTION_DAYS=7
|
||||||
value: "22"
|
## Optional: Use password instead of private key (not recommended)
|
||||||
- name: SSH_USER
|
#- SFTP_PASSWORD=password
|
||||||
value: "xxx"
|
|
||||||
- name: SSH_REMOTE_PATH
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
value: "/home/jkaninda/backups"
|
networks:
|
||||||
- name: AWS_ACCESS_KEY
|
- web
|
||||||
value: "xxxx"
|
|
||||||
- name: SSH_IDENTIFY_FILE
|
networks:
|
||||||
value: "/tmp/id_ed25519"
|
web:
|
||||||
restartPolicy: Never
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Cron Expression**: Use the `--cron-expression` flag or `BACKUP_CRON_EXPRESSION` environment variable to define the backup schedule. For example, `0 1 * * *` runs the backup daily at 1:00 AM.
|
||||||
|
- **Backup Retention**: Optionally, use the `BACKUP_RETENTION_DAYS` environment variable to automatically delete backups older than a specified number of days.
|
||||||
|
- **Security**: Always prefer private key authentication (`SFTP_IDENTITY_FILE`) over password-based authentication (`SFTP_PASSWORD`) for enhanced security.
|
||||||
|
|
||||||
|
---
|
||||||
+63
-35
@@ -5,85 +5,113 @@ parent: How Tos
|
|||||||
nav_order: 1
|
nav_order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Backup database
|
# Backup Database
|
||||||
|
|
||||||
To backup the database, you need to add `backup` subcommand to `mysql-bkup` or `bkup`.
|
To back up your database, use the `backup` command.
|
||||||
|
|
||||||
|
This section explains how to configure and run backups, including recurring backups, using Docker or Kubernetes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default Configuration
|
||||||
|
|
||||||
|
- **Storage**: By default, backups are stored locally in the `/backup` directory.
|
||||||
|
- **Compression**: Backups are compressed using `gzip` by default. Use the `--disable-compression` flag to disable compression.
|
||||||
|
- **Security**: It is recommended to create a dedicated user with read-only access for backup tasks.
|
||||||
|
|
||||||
{: .note }
|
{: .note }
|
||||||
The default storage is local storage mounted to __/backup__. The backup is compressed by default using gzip. The flag __`disable-compression`__ can be used when you need to disable backup compression.
|
The backup process supports recurring backups on Docker or Docker Swarm. On Kubernetes, it can be deployed as a CronJob.
|
||||||
|
|
||||||
{: .warning }
|
---
|
||||||
Creating a user for backup tasks who has read-only access is recommended!
|
|
||||||
|
|
||||||
The backup process can be run in scheduled mode for the recurring backups.
|
## Example: Basic Backup Configuration
|
||||||
It handles __recurring__ backups of mysql database on Docker and can be deployed as __CronJob on Kubernetes__ using local, AWS S3 or SSH compatible storage.
|
|
||||||
|
|
||||||
```yml
|
Below is an example `docker-compose.yml` configuration for backing up a database:
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup -d database
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup -d database
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backup using Docker CLI
|
---
|
||||||
|
|
||||||
```shell
|
## Backup Using Docker CLI
|
||||||
docker run --rm --network your_network_name \
|
|
||||||
|
You can also run backups directly using the Docker CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
-v $PWD/backup:/backup/ \
|
-v $PWD/backup:/backup/ \
|
||||||
-e "DB_HOST=dbhost" \
|
-e "DB_HOST=dbhost" \
|
||||||
-e "DB_USERNAME=username" \
|
-e "DB_USER=username" \
|
||||||
-e "DB_PASSWORD=password" \
|
-e "DB_PASSWORD=password" \
|
||||||
jkaninda/mysql-bkup mysql-bkup backup -d database_name
|
jkaninda/pg-bkup backup -d database_name
|
||||||
```
|
```
|
||||||
|
|
||||||
In case you need to use recurring backups, you can use `--mode scheduled` and specify the periodical backup time by adding `--period "0 1 * * *"` flag as described below.
|
---
|
||||||
|
|
||||||
```yml
|
## Recurring Backups
|
||||||
|
|
||||||
|
To schedule recurring backups, use the `--cron-expression (-e)` flag or the `BACKUP_CRON_EXPRESSION` environment variable. This allows you to define a cron schedule for automated backups.
|
||||||
|
|
||||||
|
### Example: Recurring Backup Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup -d database --cron-expression @midnight
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup -d database --mode scheduled --period "0 1 * * *"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
## Optional: Define a cron schedule for recurring backups
|
||||||
|
- BACKUP_CRON_EXPRESSION=@midnight
|
||||||
|
## Optional: Delete old backups after a specified number of days
|
||||||
|
#- BACKUP_RETENTION_DAYS=7
|
||||||
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Cron Expression**: Use the `--cron-expression (-e)` flag or `BACKUP_CRON_EXPRESSION` environment variable to define the backup schedule. For example:
|
||||||
|
- `@midnight`: Runs the backup daily at midnight.
|
||||||
|
- `0 1 * * *`: Runs the backup daily at 1:00 AM.
|
||||||
|
- **Backup Retention**: Optionally, use the `BACKUP_RETENTION_DAYS` environment variable to automatically delete backups older than a specified number of days.
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
---
|
||||||
|
title: Deploy on Kubernetes
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 9
|
||||||
|
---
|
||||||
|
|
||||||
|
# Deploy on Kubernetes
|
||||||
|
|
||||||
|
To deploy MySQL Backup on Kubernetes, you can use a `Job` for one-time backups or restores, and a `CronJob` for recurring backups.
|
||||||
|
|
||||||
|
Below are examples for different use cases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup Job to S3 Storage
|
||||||
|
|
||||||
|
This example demonstrates how to configure a Kubernetes `Job` to back up a MySQL database to an S3-compatible storage.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: backup
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- backup --storage s3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: DB_HOST
|
||||||
|
value: ""
|
||||||
|
- name: DB_NAME
|
||||||
|
value: ""
|
||||||
|
- name: DB_USER
|
||||||
|
value: ""
|
||||||
|
# Use Kubernetes Secrets for sensitive data like passwords
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: ""
|
||||||
|
- name: AWS_S3_ENDPOINT
|
||||||
|
value: "https://s3.amazonaws.com"
|
||||||
|
- name: AWS_S3_BUCKET_NAME
|
||||||
|
value: "xxx"
|
||||||
|
- name: AWS_REGION
|
||||||
|
value: "us-west-2"
|
||||||
|
- name: AWS_ACCESS_KEY
|
||||||
|
value: "xxxx"
|
||||||
|
- name: AWS_SECRET_KEY
|
||||||
|
value: "xxxx"
|
||||||
|
- name: AWS_DISABLE_SSL
|
||||||
|
value: "false"
|
||||||
|
- name: AWS_FORCE_PATH_STYLE
|
||||||
|
value: "false"
|
||||||
|
restartPolicy: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backup Job to SSH Remote Server
|
||||||
|
|
||||||
|
This example demonstrates how to configure a Kubernetes `Job` to back up a MySQL database to an SSH remote server.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: backup
|
||||||
|
spec:
|
||||||
|
ttlSecondsAfterFinished: 100
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- backup --storage ssh --disable-compression
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: DB_HOST
|
||||||
|
value: ""
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "dbname"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "postgres"
|
||||||
|
# Use Kubernetes Secrets for sensitive data like passwords
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: ""
|
||||||
|
- name: SFTP_HOST
|
||||||
|
value: "xxx"
|
||||||
|
- name: SFTP_PORT
|
||||||
|
value: "22"
|
||||||
|
- name: SFTP_USER
|
||||||
|
value: "xxx"
|
||||||
|
- name: SFTP_PASSWORD
|
||||||
|
value: "xxxx"
|
||||||
|
- name: REMOTE_PATH
|
||||||
|
value: "/home/toto/backup"
|
||||||
|
# Optional: Required if you want to encrypt your backup
|
||||||
|
- name: GPG_PASSPHRASE
|
||||||
|
value: "xxxx"
|
||||||
|
restartPolicy: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Restore Job
|
||||||
|
|
||||||
|
This example demonstrates how to configure a Kubernetes `Job` to restore a MySQL database from a backup stored on an SSH remote server.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: restore-job
|
||||||
|
spec:
|
||||||
|
ttlSecondsAfterFinished: 100
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- restore --storage ssh --file store_20231219_022941.sql.gz
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: DB_HOST
|
||||||
|
value: ""
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "dbname"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "postgres"
|
||||||
|
# Use Kubernetes Secrets for sensitive data like passwords
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: ""
|
||||||
|
- name: SFTP_HOST
|
||||||
|
value: "xxx"
|
||||||
|
- name: SFTP_PORT
|
||||||
|
value: "22"
|
||||||
|
- name: SFTP_USER
|
||||||
|
value: "xxx"
|
||||||
|
- name: SFTP_PASSWORD
|
||||||
|
value: "xxxx"
|
||||||
|
- name: REMOTE_PATH
|
||||||
|
value: "/home/toto/backup"
|
||||||
|
# Optional: Required if your backup was encrypted
|
||||||
|
#- name: GPG_PASSPHRASE
|
||||||
|
# value: "xxxx"
|
||||||
|
restartPolicy: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recurring Backup with CronJob
|
||||||
|
|
||||||
|
This example demonstrates how to configure a Kubernetes `CronJob` for recurring backups to an SSH remote server.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: backup-job
|
||||||
|
spec:
|
||||||
|
schedule: "* * * * *"
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- backup --storage ssh --disable-compression
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: DB_HOST
|
||||||
|
value: ""
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "test"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "postgres"
|
||||||
|
# Use Kubernetes Secrets for sensitive data like passwords
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: ""
|
||||||
|
- name: SFTP_HOST
|
||||||
|
value: "192.168.1.16"
|
||||||
|
- name: SFTP_PORT
|
||||||
|
value: "2222"
|
||||||
|
- name: SFTP_USER
|
||||||
|
value: "jkaninda"
|
||||||
|
- name: REMOTE_PATH
|
||||||
|
value: "/config/backup"
|
||||||
|
- name: SFTP_PASSWORD
|
||||||
|
value: "password"
|
||||||
|
# Optional: Required if you want to encrypt your backup
|
||||||
|
#- name: GPG_PASSPHRASE
|
||||||
|
# value: "xxx"
|
||||||
|
restartPolicy: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Kubernetes Rootless Deployment
|
||||||
|
|
||||||
|
This example demonstrates how to run the backup container in a rootless environment, suitable for platforms like OpenShift.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: backup-job
|
||||||
|
spec:
|
||||||
|
schedule: "* * * * *"
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 3000
|
||||||
|
fsGroup: 2000
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- backup --storage ssh --disable-compression
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: DB_HOST
|
||||||
|
value: ""
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "test"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "postgres"
|
||||||
|
# Use Kubernetes Secrets for sensitive data like passwords
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: ""
|
||||||
|
- name: SFTP_HOST
|
||||||
|
value: "192.168.1.16"
|
||||||
|
- name: SFTP_PORT
|
||||||
|
value: "2222"
|
||||||
|
- name: SFTP_USER
|
||||||
|
value: "jkaninda"
|
||||||
|
- name: REMOTE_PATH
|
||||||
|
value: "/config/backup"
|
||||||
|
- name: SFTP_PASSWORD
|
||||||
|
value: "password"
|
||||||
|
# Optional: Required if you want to encrypt your backup
|
||||||
|
#- name: GPG_PASSPHRASE
|
||||||
|
# value: "xxx"
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrate Database
|
||||||
|
|
||||||
|
This example demonstrates how to configure a Kubernetes `Job` to migrate a MySQL database from one server to another.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: migrate-db
|
||||||
|
spec:
|
||||||
|
ttlSecondsAfterFinished: 100
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- migrate
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
## Source Database
|
||||||
|
- name: DB_HOST
|
||||||
|
value: "postgres"
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: DB_NAME
|
||||||
|
value: "dbname"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "username"
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: "password"
|
||||||
|
## Target Database
|
||||||
|
- name: TARGET_DB_HOST
|
||||||
|
value: "target-postgres"
|
||||||
|
- name: TARGET_DB_PORT
|
||||||
|
value: "3306"
|
||||||
|
- name: TARGET_DB_NAME
|
||||||
|
value: "dbname"
|
||||||
|
- name: TARGET_DB_USER
|
||||||
|
value: "username"
|
||||||
|
- name: TARGET_DB_PASSWORD
|
||||||
|
value: "password"
|
||||||
|
restartPolicy: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Security**: Always use Kubernetes Secrets for sensitive data like passwords and access keys.
|
||||||
|
- **Resource Limits**: Adjust resource limits (`memory` and `cpu`) based on your workload requirements.
|
||||||
|
- **Cron Schedule**: Use standard cron expressions for scheduling recurring backups.
|
||||||
|
- **Rootless Deployment**: The image supports running in rootless environments, making it suitable for platforms like OpenShift.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: Update deprecated configurations
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 11
|
||||||
|
---
|
||||||
+105
-23
@@ -2,53 +2,135 @@
|
|||||||
title: Encrypt backups using GPG
|
title: Encrypt backups using GPG
|
||||||
layout: default
|
layout: default
|
||||||
parent: How Tos
|
parent: How Tos
|
||||||
nav_order: 7
|
nav_order: 8
|
||||||
---
|
---
|
||||||
# Encrypt backup
|
# Encrypt Backup
|
||||||
|
|
||||||
The image supports encrypting backups using GPG out of the box. In case a `GPG_PASSPHRASE` environment variable is set, the backup archive will be encrypted using the given key and saved as a sql.gpg file instead or sql.gz.gpg.
|
The image supports encrypting backups using one of two methods: **GPG with a passphrase** or **GPG with a public key**. When a `GPG_PASSPHRASE` or `GPG_PUBLIC_KEY` environment variable is set, the backup archive will be encrypted and saved as a `.sql.gpg` or `.sql.gz.gpg` file.
|
||||||
|
|
||||||
{: .warning }
|
{: .warning }
|
||||||
To restore an encrypted backup, you need to provide the same GPG passphrase used during backup process.
|
To restore an encrypted backup, you must provide the same GPG passphrase or private key used during the backup process.
|
||||||
|
|
||||||
To decrypt manually, you need to install gnupg
|
---
|
||||||
|
|
||||||
### Decrypt backup
|
## Key Features
|
||||||
|
|
||||||
```shell
|
- **Cipher Algorithm**: `aes256`
|
||||||
gpg --batch --passphrase "my-passphrase" \
|
- **Automatic Restoration**: Backups encrypted with a GPG passphrase can be restored automatically without manual decryption.
|
||||||
--output database_20240730_044201.sql.gz \
|
- **Manual Decryption**: Backups encrypted with a GPG public key require manual decryption before restoration.
|
||||||
--decrypt database_20240730_044201.sql.gz.gpg
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup
|
---
|
||||||
|
|
||||||
```yml
|
## Using GPG Passphrase
|
||||||
|
|
||||||
|
To encrypt backups using a GPG passphrase, set the `GPG_PASSPHRASE` environment variable. The backup will be encrypted and can be restored automatically.
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup -d database
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup -d database
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
## Required to encrypt backup
|
## Required to encrypt backup
|
||||||
- GPG_PASSPHRASE=my-secure-passphrase
|
- GPG_PASSPHRASE=my-secure-passphrase
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
# Ensure the pg-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using GPG Public Key
|
||||||
|
|
||||||
|
To encrypt backups using a GPG public key, set the `GPG_PUBLIC_KEY` environment variable to the path of your public key file. To restore such backups, set `GPG_PRIVATE_KEY` (a file path or the inline armored key; `/config/gnupg/private_key.asc` is picked up automatically) and, if the key is protected, `GPG_PRIVATE_KEY_PASSPHRASE` (defaults to `GPG_PASSPHRASE`).
|
||||||
|
|
||||||
|
### Example Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup -d database
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
- ./public_key.asc:/config/public_key.asc
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## Required to encrypt backup
|
||||||
|
- GPG_PUBLIC_KEY=/config/public_key.asc
|
||||||
|
# Ensure the pg-bkup container is connected to the same network as your database
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Decryption
|
||||||
|
|
||||||
|
If you encrypted your backup using a GPG public key, you must manually decrypt it before restoration. Use the `gnupg` tool for decryption.
|
||||||
|
|
||||||
|
### Decrypt Using a Passphrase
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --batch --passphrase "my-passphrase" \
|
||||||
|
--output database_20240730_044201.sql.gz \
|
||||||
|
--decrypt database_20240730_044201.sql.gz.gpg
|
||||||
|
```
|
||||||
|
|
||||||
|
### Decrypt Using a Private Key
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gpg --output database_20240730_044201.sql.gz \
|
||||||
|
--decrypt database_20240730_044201.sql.gz.gpg
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Automatic Restoration**: Backups encrypted with a GPG passphrase can be restored directly without manual decryption.
|
||||||
|
- **Private key restore**: Backups encrypted with a GPG public key restore directly when `GPG_PRIVATE_KEY` is set; manual decryption also works.
|
||||||
|
- **Fail closed**: if `GPG_PUBLIC_KEY` (or any configured key) cannot be read, the backup fails instead of being stored unencrypted.
|
||||||
|
- **Security**: Always keep your GPG passphrase and private key secure. Use Kubernetes Secrets or other secure methods to manage sensitive data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using age
|
||||||
|
|
||||||
|
[age](https://age-encryption.org) is a modern alternative to GPG. Set `AGE_RECIPIENTS` (one or more `age1...` or `ssh-ed25519 ...` public keys, comma or newline separated, or `AGE_RECIPIENTS_FILE`) and backups are saved as `.sql.gz.age`. Restore with the matching identity in `AGE_IDENTITY` (`AGE-SECRET-KEY-...`) or `AGE_IDENTITY_FILE` (default `/config/age/identity.txt`).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- AGE_RECIPIENTS=age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||||
|
# restore:
|
||||||
|
# - AGE_IDENTITY_FILE=/run/secrets/age_identity
|
||||||
|
```
|
||||||
|
|
||||||
|
`AGE_PASSPHRASE` encrypts with a passphrase instead (it needs about 256 MiB of memory for the key derivation). `ENCRYPTION=auto|none|gpg|age` chooses explicitly; with `auto` (default) GPG wins when both GPG and age keys are set. Decrypt by hand with `age -d -i key.txt file.sql.gz.age | gunzip`.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
title: Migrate database
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
# Migrate Database
|
||||||
|
|
||||||
|
To migrate a MySQL database from a source to a target database, you can use the `migrate` command. This feature simplifies the process by combining the backup and restore operations into a single step.
|
||||||
|
|
||||||
|
{: .note }
|
||||||
|
The `migrate` command eliminates the need for separate backup and restore operations. It directly transfers data from the source database to the target database.
|
||||||
|
|
||||||
|
{: .warning }
|
||||||
|
The `migrate` operation is **irreversible**. Always back up your target database before performing this action.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Steps
|
||||||
|
|
||||||
|
1. **Source Database**: Provide connection details for the source database.
|
||||||
|
2. **Target Database**: Provide connection details for the target database.
|
||||||
|
3. **Run the Migration**: Use the `migrate` command to initiate the migration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Docker Compose Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for migrating a database:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysqlbkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: migrate
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
## Source Database
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
|
||||||
|
## Target Database
|
||||||
|
- TARGET_DB_HOST=target-mysql
|
||||||
|
- TARGET_DB_PORT=3306
|
||||||
|
- TARGET_DB_NAME=dbname
|
||||||
|
- TARGET_DB_USER=username
|
||||||
|
- TARGET_DB_PASSWORD=password
|
||||||
|
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migrate Database Using Docker CLI
|
||||||
|
|
||||||
|
You can also run the migration directly using the Docker CLI. Below is an example:
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Save your source and target database connection details in an environment file (e.g., `your-env`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
## Source Database
|
||||||
|
DB_HOST=postgres
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=dbname
|
||||||
|
DB_USER=username
|
||||||
|
DB_PASSWORD=password
|
||||||
|
|
||||||
|
## Target Database
|
||||||
|
TARGET_DB_HOST=target-mysql
|
||||||
|
TARGET_DB_PORT=3306
|
||||||
|
TARGET_DB_NAME=dbname
|
||||||
|
TARGET_DB_USER=username
|
||||||
|
TARGET_DB_PASSWORD=password
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run the Migration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
--env-file your-env \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
jkaninda/mysql-bkup migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Streaming**: the dump is piped straight into the target server; no file is written. The target database is created if it does not exist, and `TARGET_DB_NAME` defaults to the source name.
|
||||||
|
- **All databases**: `migrate --all-databases` copies every database (system databases excluded, `--exclude-db` / `BACKUP_EXCLUDE_DATABASES` honoured) under the same names.
|
||||||
|
- **MySQL and MariaDB**: source and target may be different flavours; each side uses its own client. Objects that only exist in one flavour (for example MySQL-only collations on old MariaDB versions) can make a cross-flavour migration fail.
|
||||||
|
- **Irreversible Operation**: The `migrate` command directly transfers data from the source to the target database. Ensure you have a backup of the target database before proceeding.
|
||||||
|
- **Network Configuration**: Ensure the `mysql-bkup` container is connected to the same network as your source and target databases.
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
title: Run multiple database backup schedules in the same container
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 11
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
# Multiple Backup Schedules
|
||||||
|
|
||||||
|
This tool supports running multiple database backup schedules within the same container.
|
||||||
|
You can configure these schedules with different settings using a **configuration file**. This flexibility allows you to manage backups for multiple databases efficiently.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration File Setup
|
||||||
|
|
||||||
|
The configuration file can be mounted into the container at `/config/config.yaml`, `/config/config.yml`, or specified via the `BACKUP_CONFIG_FILE` environment variable.
|
||||||
|
|
||||||
|
### Key Features:
|
||||||
|
- **Global Environment Variables**: Use these for databases that share the same configuration.
|
||||||
|
- **Database-Specific Overrides**: Override global settings for individual databases by specifying them in the configuration file or using the database name as a prefix or suffix in the variable name (e.g., `DB_HOST_DATABASENAME` or `DATABASENAME_DB_HOST`).
|
||||||
|
- **Global Cron Expression**: Define a global `cronExpression` in the configuration file to schedule backups for all databases. If omitted, backups will run immediately.
|
||||||
|
- **Configuration File Path**: Specify the configuration file path using:
|
||||||
|
- The `BACKUP_CONFIG_FILE` environment variable.
|
||||||
|
- The `--config` or `-c` flag for the backup command.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration File Example
|
||||||
|
|
||||||
|
Below is an example configuration file (`config.yaml`) that defines multiple databases and their respective backup settings:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Optional: Define a global cron expression for scheduled backups.
|
||||||
|
# Example: "@every 20m" (runs every 20 minutes). If omitted, backups run immediately.
|
||||||
|
cronExpression: "" # Optional: Define a global cron expression for scheduled backups.
|
||||||
|
backupRescueMode: false # Optional: Set to true to enable rescue mode for backups.
|
||||||
|
databases:
|
||||||
|
- host: mysql1 # Optional: Overrides DB_HOST or uses DB_HOST_DATABASE1.
|
||||||
|
port: 3306 # Optional: Default is 5432. Overrides DB_PORT or uses DB_PORT_DATABASE1.
|
||||||
|
name: database1 # Required: Database name.
|
||||||
|
user: database1 # Optional: Overrides DB_USER or uses DB_USER_DATABASE1.
|
||||||
|
password: password # Optional: Overrides DB_PASSWORD or uses DB_PASSWORD_DATABASE1.
|
||||||
|
path: /s3-path/database1 # Required: Backup path for SSH, FTP, or S3 (e.g., /home/toto/backup/).
|
||||||
|
|
||||||
|
- host: mysql2 # Optional: Overrides DB_HOST or uses DB_HOST_LLAP.
|
||||||
|
port: 3306 # Optional: Default is 5432. Overrides DB_PORT or uses DB_PORT_LLAP.
|
||||||
|
name: lldap # Required: Database name.
|
||||||
|
user: lldap # Optional: Overrides DB_USER or uses DB_USER_LLAP.
|
||||||
|
password: password # Optional: Overrides DB_PASSWORD or uses DB_PASSWORD_LLAP.
|
||||||
|
path: /s3-path/lldap # Required: Backup path for SSH, FTP, or S3 (e.g., /home/toto/backup/).
|
||||||
|
|
||||||
|
- host: mysql3 # Optional: Overrides DB_HOST or uses DB_HOST_KEYCLOAK.
|
||||||
|
port: 3306 # Optional: Default is 5432. Overrides DB_PORT or uses DB_PORT_KEYCLOAK.
|
||||||
|
name: keycloak # Required: Database name.
|
||||||
|
user: keycloak # Optional: Overrides DB_USER or uses DB_USER_KEYCLOAK.
|
||||||
|
password: password # Optional: Overrides DB_PASSWORD or uses DB_PASSWORD_KEYCLOAK.
|
||||||
|
path: /s3-path/keycloak # Required: Backup path for SSH, FTP, or S3 (e.g., /home/toto/backup/).
|
||||||
|
|
||||||
|
- host: mysql4 # Optional: Overrides DB_HOST or uses DB_HOST_JOPLIN.
|
||||||
|
port: 3306 # Optional: Default is 5432. Overrides DB_PORT or uses DB_PORT_JOPLIN.
|
||||||
|
name: joplin # Required: Database name.
|
||||||
|
user: joplin # Optional: Overrides DB_USER or uses DB_USER_JOPLIN.
|
||||||
|
password: password # Optional: Overrides DB_PASSWORD or uses DB_PASSWORD_JOPLIN.
|
||||||
|
path: /s3-path/joplin # Required: Backup path for SSH, FTP, or S3 (e.g., /home/toto/backup/).
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker Compose Configuration
|
||||||
|
|
||||||
|
To use the configuration file in a Docker Compose setup, mount the file and specify its path using the `BACKUP_CONFIG_FILE` environment variable.
|
||||||
|
|
||||||
|
### Example: Docker Compose File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, lock your image tag to a specific release version
|
||||||
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for available releases.
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup #--config /backup/config.yaml # config file
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup # Mount the backup directory
|
||||||
|
- ./config.yaml:/backup/config.yaml # Mount the configuration file
|
||||||
|
environment:
|
||||||
|
## Specify the path to the configuration file
|
||||||
|
- BACKUP_CONFIG_FILE=/backup/config.yaml
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
---
|
||||||
|
title: Receive notifications
|
||||||
|
layout: default
|
||||||
|
parent: How Tos
|
||||||
|
nav_order: 13
|
||||||
|
---
|
||||||
|
|
||||||
|
# Receive Notifications
|
||||||
|
|
||||||
|
You can configure the system to send email (SMTP or Posta) or Telegram notifications when a backup succeeds or fails.
|
||||||
|
|
||||||
|
This section explains how to set up and customize notifications.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Email Notifications
|
||||||
|
|
||||||
|
To send email notifications, provide SMTP credentials, a sender address, and recipient addresses. Notifications will be sent for both successful and failed backup runs.
|
||||||
|
|
||||||
|
### Example: Email Notification Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## SMTP Configuration
|
||||||
|
- MAIL_HOST=smtp.example.com
|
||||||
|
- MAIL_PORT=587
|
||||||
|
- MAIL_USERNAME=your-email@example.com
|
||||||
|
- MAIL_PASSWORD=your-email-password
|
||||||
|
- MAIL_FROM=Backup Jobs <backup@example.com>
|
||||||
|
## Multiple recipients separated by a comma
|
||||||
|
- MAIL_TO=me@example.com,team@example.com,manager@example.com
|
||||||
|
## Only for SMTP servers with a self-signed certificate
|
||||||
|
#- MAIL_INSECURE_SKIP_VERIFY=true
|
||||||
|
## Time format for notifications
|
||||||
|
- TIME_FORMAT=2006-01-02 at 15:04:05
|
||||||
|
## Backup reference (e.g., database/cluster name or server name)
|
||||||
|
- BACKUP_REFERENCE=database/Paris cluster
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Posta Notifications
|
||||||
|
|
||||||
|
[Posta](https://github.com/goposta/posta) can deliver the notification emails instead of an SMTP server. Set the Posta URL, an API key with the `send` scope and the recipients. By default the same HTML email is sent (with a plain-text version). To use a template stored in Posta instead, set `POSTA_TEMPLATE` (and optionally `POSTA_TEMPLATE_ERROR` for failures); the notification fields (`database`, `file`, `duration`, `storage`, `location`, `size`, `backup_size`, `sha256`, `encryption`, `error`, `time`, `success`, `tool`...) are passed as template data.
|
||||||
|
|
||||||
|
### Example: Posta Notification Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=db
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## Posta
|
||||||
|
- POSTA_URL=https://posta.example.com
|
||||||
|
- POSTA_API_KEY=psk_your_api_key
|
||||||
|
- POSTA_FROM=Backup Jobs <backup@example.com>
|
||||||
|
## Multiple recipients separated by a comma (default MAIL_TO)
|
||||||
|
- POSTA_TO=me@example.com,team@example.com
|
||||||
|
## Optional: templates stored in Posta
|
||||||
|
#- POSTA_TEMPLATE=backup-success
|
||||||
|
#- POSTA_TEMPLATE_ERROR=backup-failure
|
||||||
|
#- POSTA_LANGUAGE=en
|
||||||
|
## Only for account-wide API keys
|
||||||
|
#- POSTA_WORKSPACE_ID=42
|
||||||
|
## Only notify failures
|
||||||
|
#- NOTIFY_ON=failure
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Telegram Notifications
|
||||||
|
|
||||||
|
To send Telegram notifications, provide your bot token and chat ID. Notifications will be sent for both successful and failed backup runs.
|
||||||
|
|
||||||
|
### Example: Telegram Notification Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=database
|
||||||
|
- DB_USER=username
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
## Telegram Configuration
|
||||||
|
- TG_TOKEN=[BOT ID]:[BOT TOKEN]
|
||||||
|
- TG_CHAT_ID=your-chat-id
|
||||||
|
## Time format for notifications
|
||||||
|
- TIME_FORMAT=2006-01-02 at 15:04:05
|
||||||
|
## Backup reference (e.g., database/cluster name or server name)
|
||||||
|
- BACKUP_REFERENCE=database/Paris cluster
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customize Notifications
|
||||||
|
|
||||||
|
You can customize the title and body of notifications using Go templates. Template files must be mounted inside the container at `/config/templates`. The following templates are supported:
|
||||||
|
|
||||||
|
- `email.tmpl`: Template for successful email notifications.
|
||||||
|
- `telegram.tmpl`: Template for successful Telegram notifications.
|
||||||
|
- `email-error.tmpl`: Template for failed email notifications.
|
||||||
|
- `telegram-error.tmpl`: Template for failed Telegram notifications.
|
||||||
|
|
||||||
|
### Template Data
|
||||||
|
|
||||||
|
The following data is passed to the templates:
|
||||||
|
|
||||||
|
- `Database`: Database name.
|
||||||
|
- `StartTime`: Backup start time.
|
||||||
|
- `EndTime`: Backup end time.
|
||||||
|
- `Storage`: Backup storage type (e.g., local, S3, SSH).
|
||||||
|
- `BackupLocation`: Backup file location.
|
||||||
|
- `BackupSize`: Backup file size in bytes.
|
||||||
|
- `BackupReference`: Backup reference (e.g., database/cluster name or server name).
|
||||||
|
- `Error`: Error message (only for error templates).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Example Templates
|
||||||
|
|
||||||
|
#### `email.tmpl` (Successful Backup)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h2>Hi,</h2>
|
||||||
|
<p>Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.</p>
|
||||||
|
<h3>Backup Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Database Name: {{.Database}}</li>
|
||||||
|
<li>Backup Start Time: {{.StartTime}}</li>
|
||||||
|
<li>Backup End Time: {{.EndTime}}</li>
|
||||||
|
<li>Backup Storage: {{.Storage}}</li>
|
||||||
|
<li>Backup Location: {{.BackupLocation}}</li>
|
||||||
|
<li>Backup Size: {{.BackupSize}} bytes</li>
|
||||||
|
<li>Backup Reference: {{.BackupReference}}</li>
|
||||||
|
</ul>
|
||||||
|
<p>Best regards,</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `telegram.tmpl` (Successful Backup)
|
||||||
|
|
||||||
|
```html
|
||||||
|
✅ Database Backup Notification – {{.Database}}
|
||||||
|
Hi,
|
||||||
|
Backup of the {{.Database}} database has been successfully completed on {{.EndTime}}.
|
||||||
|
|
||||||
|
Backup Details:
|
||||||
|
- Database Name: {{.Database}}
|
||||||
|
- Backup Start Time: {{.StartTime}}
|
||||||
|
- Backup End Time: {{.EndTime}}
|
||||||
|
- Backup Storage: {{.Storage}}
|
||||||
|
- Backup Location: {{.BackupLocation}}
|
||||||
|
- Backup Size: {{.BackupSize}} bytes
|
||||||
|
- Backup Reference: {{.BackupReference}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `email-error.tmpl` (Failed Backup)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>🔴 Urgent: Database Backup Failure Notification</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>Hi,</h2>
|
||||||
|
<p>An error occurred during database backup.</p>
|
||||||
|
<h3>Failure Details:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Error Message: {{.Error}}</li>
|
||||||
|
<li>Date: {{.EndTime}}</li>
|
||||||
|
<li>Backup Reference: {{.BackupReference}}</li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `telegram-error.tmpl` (Failed Backup)
|
||||||
|
|
||||||
|
```html
|
||||||
|
🔴 Urgent: Database Backup Failure Notification
|
||||||
|
|
||||||
|
An error occurred during database backup.
|
||||||
|
Failure Details:
|
||||||
|
|
||||||
|
Error Message: {{.Error}}
|
||||||
|
Date: {{.EndTime}}
|
||||||
|
Backup Reference: {{.BackupReference}}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **SMTP Configuration**: Ensure your SMTP server supports TLS unless `MAIL_INSECURE_SKIP_VERIFY` is set to `true`. `MAIL_TLS=starttls|tls|none` selects the TLS mode. The 1.x variable `MAIL_SKIP_TLS` is still read with its documented meaning (`true` = skip certificate verification); 1.x had it inverted. Slack (`SLACK_WEBHOOK_URL`), Discord (`DISCORD_WEBHOOK_URL`), ntfy (`NTFY_URL`), generic webhooks (`WEBHOOK_URL`, `WEBHOOK_SECRET`) and healthchecks.io pings (`HEALTHCHECK_PING_URL`) are also available; `NOTIFY_ON=always|failure|success` filters them.
|
||||||
|
- **Telegram Configuration**: Obtain your bot token and chat ID from Telegram.
|
||||||
|
- **Custom Templates**: Mount custom templates to `/config/templates` to override default notifications.
|
||||||
|
- **Time Format**: Use the `TIME_FORMAT` environment variable to customize the timestamp format in notifications.
|
||||||
@@ -2,100 +2,74 @@
|
|||||||
title: Restore database from AWS S3
|
title: Restore database from AWS S3
|
||||||
layout: default
|
layout: default
|
||||||
parent: How Tos
|
parent: How Tos
|
||||||
nav_order: 5
|
nav_order: 6
|
||||||
---
|
---
|
||||||
|
|
||||||
# Restore database from S3 storage
|
# Restore Database from S3 Storage
|
||||||
|
|
||||||
To restore the database, you need to add `restore` subcommand to `mysql-bkup` or `bkup` and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
|
To restore a MySQL database from a backup stored in S3, use the `restore` command and specify the backup file with the `--file` flag. The system supports the following file formats:
|
||||||
|
|
||||||
{: .note }
|
- `.sql` (uncompressed SQL dump)
|
||||||
It supports __.sql__ and __.sql.gz__ compressed file.
|
- `.sql.gz` (gzip-compressed SQL dump)
|
||||||
|
- `.sql.gpg` (GPG-encrypted SQL dump)
|
||||||
|
- `.sql.gz.gpg` (GPG-encrypted and gzip-compressed SQL dump)
|
||||||
|
|
||||||
### Restore
|
---
|
||||||
|
|
||||||
```yml
|
## Configuration Steps
|
||||||
|
|
||||||
|
1. **Specify the Backup File**: Use the `--file` flag to specify the backup file to restore.
|
||||||
|
2. **Set the Storage Type**: Add the `--storage s3` flag to indicate that the backup is stored in S3.
|
||||||
|
3. **Provide S3 Configuration**: Include the necessary AWS S3 credentials and configuration.
|
||||||
|
4. **Provide Database Credentials**: Ensure the correct database connection details are provided.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Restore from S3 Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for restoring a database from S3 storage:
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: restore --storage s3 -d my-database -f store_20231219_022941.sql.gz --path /my-custom-path
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup restore --storage s3 -d my-database -f store_20231219_022941.sql.gz --path /my-custom-path
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup # Mount the directory for local operations (if needed)
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
## AWS configurations
|
## AWS S3 Configuration
|
||||||
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
- AWS_S3_BUCKET_NAME=backup
|
- AWS_S3_BUCKET_NAME=backup
|
||||||
- AWS_REGION="us-west-2"
|
- AWS_REGION=us-west-2
|
||||||
- AWS_ACCESS_KEY=xxxx
|
- AWS_ACCESS_KEY=xxxx
|
||||||
- AWS_SECRET_KEY=xxxxx
|
- AWS_SECRET_KEY=xxxxx
|
||||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
## Optional: Disable SSL for S3 alternatives like Minio
|
||||||
- AWS_DISABLE_SSL="false"
|
- AWS_DISABLE_SSL=false
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
## Optional: Enable path-style access for S3 alternatives like Minio
|
||||||
|
- AWS_FORCE_PATH_STYLE=false
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
## Restore on Kubernetes
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
### Simple Kubernetes CronJob usage:
|
- **Supported File Formats**: The restore process supports `.sql`, `.sql.gz`, `.sql.gpg`, and `.sql.gz.gpg` files.
|
||||||
|
- **S3 Path**: Use the `--path` flag to specify the folder within the S3 bucket where the backup file is located.
|
||||||
```yaml
|
- **Encrypted Backups**: If the backup is encrypted with GPG, ensure the `GPG_PASSPHRASE` environment variable is set for automatic decryption.
|
||||||
apiVersion: batch/v1
|
- **S3 Alternatives**: For S3-compatible storage like Minio, set `AWS_DISABLE_SSL` and `AWS_FORCE_PATH_STYLE` as needed.
|
||||||
kind: CronJob
|
- **Network Configuration**: Ensure the `pg-bkup` container is connected to the same network as your database.
|
||||||
metadata:
|
|
||||||
name: bkup-job
|
|
||||||
spec:
|
|
||||||
schedule: "0 1 * * *"
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: mysql-bkup
|
|
||||||
image: jkaninda/mysql-bkup
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup restore -s s3 --path /custom_path -f store_20231219_022941.sql.gz
|
|
||||||
env:
|
|
||||||
- name: DB_PORT
|
|
||||||
value: "3306"
|
|
||||||
- name: DB_HOST
|
|
||||||
value: ""
|
|
||||||
- name: DB_NAME
|
|
||||||
value: ""
|
|
||||||
- name: DB_USERNAME
|
|
||||||
value: ""
|
|
||||||
# Please use secret!
|
|
||||||
- name: DB_PASSWORD
|
|
||||||
value: ""
|
|
||||||
- name: AWS_S3_ENDPOINT
|
|
||||||
value: "https://s3.amazonaws.com"
|
|
||||||
- name: AWS_S3_BUCKET_NAME
|
|
||||||
value: "xxx"
|
|
||||||
- name: AWS_REGION
|
|
||||||
value: "us-west-2"
|
|
||||||
- name: AWS_ACCESS_KEY
|
|
||||||
value: "xxxx"
|
|
||||||
- name: AWS_SECRET_KEY
|
|
||||||
value: "xxxx"
|
|
||||||
- name: AWS_DISABLE_SSL
|
|
||||||
value: "false"
|
|
||||||
restartPolicy: OnFailure
|
|
||||||
```
|
|
||||||
@@ -2,97 +2,74 @@
|
|||||||
title: Restore database from SSH
|
title: Restore database from SSH
|
||||||
layout: default
|
layout: default
|
||||||
parent: How Tos
|
parent: How Tos
|
||||||
nav_order: 6
|
nav_order: 7
|
||||||
---
|
---
|
||||||
# Restore database from SSH remote server
|
|
||||||
|
|
||||||
To restore the database from your remote server, you need to add `restore` subcommand to `mysql-bkup` or `bkup` and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
|
# Restore Database from SSH Remote Server
|
||||||
|
|
||||||
{: .note }
|
To restore a MySQL database from a backup stored on an SSH remote server, use the `restore` command and specify the backup file with the `--file` flag. The system supports the following file formats:
|
||||||
It supports __.sql__ and __.sql.gz__ compressed file.
|
|
||||||
|
|
||||||
### Restore
|
- `.sql` (uncompressed SQL dump)
|
||||||
|
- `.sql.gz` (gzip-compressed SQL dump)
|
||||||
|
- `.sql.gpg` (GPG-encrypted SQL dump)
|
||||||
|
- `.sql.gz.gpg` (GPG-encrypted and gzip-compressed SQL dump)
|
||||||
|
|
||||||
```yml
|
---
|
||||||
|
|
||||||
|
## Configuration Steps
|
||||||
|
|
||||||
|
1. **Specify the Backup File**: Use the `--file` flag to specify the backup file to restore.
|
||||||
|
2. **Set the Storage Type**: Add the `--storage ssh` flag to indicate that the backup is stored on an SSH remote server.
|
||||||
|
3. **Provide SSH Configuration**: Include the necessary SSH credentials and configuration.
|
||||||
|
4. **Provide Database Credentials**: Ensure the correct database connection details are provided.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Restore from SSH Remote Server Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for restoring a database from an SSH remote server:
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: restore --storage ssh -d my-database -f store_20231219_022941.sql.gz --path /home/jkaninda/backups
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup restore --storage ssh -d my-database -f store_20231219_022941.sql.gz --path /home/jkaninda/backups
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup # Mount the directory for local operations (if needed)
|
||||||
|
- ./id_ed25519:/tmp/id_ed25519 # Mount the SSH private key file
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=postgres
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
## SSH config
|
## SSH Configuration
|
||||||
- SSH_HOST_NAME="hostname"
|
- SFTP_HOST=hostname
|
||||||
- SSH_PORT=22
|
- SFTP_PORT=22
|
||||||
- SSH_USER=user
|
- SFTP_USER=user
|
||||||
- SSH_REMOTE_PATH=/home/jkaninda/backups
|
- REMOTE_PATH=/home/jkaninda/backups
|
||||||
- SSH_IDENTIFY_FILE=/tmp/id_ed25519
|
- SFTP_IDENTITY_FILE=/tmp/id_ed25519
|
||||||
## We advise you to use a private jey instead of password
|
- SFTP_KNOWN_HOSTS=/config/known_hosts
|
||||||
#- SSH_PASSWORD=password
|
## Optional: Use password instead of private key (not recommended)
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
#- SFTP_PASSWORD=password
|
||||||
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
## Restore on Kubernetes
|
|
||||||
|
|
||||||
Simple Kubernetes CronJob usage:
|
---
|
||||||
|
|
||||||
```yaml
|
## Key Notes
|
||||||
apiVersion: batch/v1
|
|
||||||
kind: CronJob
|
- **Supported File Formats**: The restore process supports `.sql`, `.sql.gz`, `.sql.gpg`, and `.sql.gz.gpg` files.
|
||||||
metadata:
|
- **SSH Path**: Use the `--path` flag to specify the folder on the SSH remote server where the backup file is located.
|
||||||
name: bkup-job
|
- **Encrypted Backups**: If the backup is encrypted with GPG, ensure the `GPG_PASSPHRASE` environment variable is set for automatic decryption.
|
||||||
spec:
|
- **SSH Authentication**: Use a private key (`SFTP_IDENTITY_FILE`) for SSH authentication instead of a password for better security. Host keys are verified against `SFTP_KNOWN_HOSTS` (or set `SFTP_INSECURE_IGNORE_HOST_KEY=true`).
|
||||||
schedule: "0 1 * * *"
|
- **Network Configuration**: Ensure the `mysql-bkup` container is connected to the same network as your database.
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: mysql-bkup
|
|
||||||
image: jkaninda/mysql-bkup
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup restore -s ssh -f store_20231219_022941.sql.gz
|
|
||||||
env:
|
|
||||||
- name: DB_PORT
|
|
||||||
value: "3306"
|
|
||||||
- name: DB_HOST
|
|
||||||
value: ""
|
|
||||||
- name: DB_NAME
|
|
||||||
value: ""
|
|
||||||
- name: DB_USERNAME
|
|
||||||
value: ""
|
|
||||||
# Please use secret!
|
|
||||||
- name: DB_PASSWORD
|
|
||||||
value: ""
|
|
||||||
- name: SSH_HOST_NAME
|
|
||||||
value: ""
|
|
||||||
- name: SSH_PORT
|
|
||||||
value: "22"
|
|
||||||
- name: SSH_USER
|
|
||||||
value: "xxx"
|
|
||||||
- name: SSH_REMOTE_PATH
|
|
||||||
value: "/home/jkaninda/backups"
|
|
||||||
- name: AWS_ACCESS_KEY
|
|
||||||
value: "xxxx"
|
|
||||||
- name: SSH_IDENTIFY_FILE
|
|
||||||
value: "/tmp/id_ed25519"
|
|
||||||
restartPolicy: Never
|
|
||||||
```
|
|
||||||
+40
-19
@@ -2,42 +2,63 @@
|
|||||||
title: Restore database
|
title: Restore database
|
||||||
layout: default
|
layout: default
|
||||||
parent: How Tos
|
parent: How Tos
|
||||||
nav_order: 4
|
nav_order: 5
|
||||||
---
|
---
|
||||||
|
|
||||||
# Restore database
|
|
||||||
|
|
||||||
To restore the database, you need to add `restore` subcommand to `mysql-bkup` or `bkup` and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
|
# Restore Database
|
||||||
|
|
||||||
{: .note }
|
To restore a MySQL database, use the `restore` command and specify the backup file to restore with the `--file` flag.
|
||||||
It supports __.sql__ and __.sql.gz__ compressed file.
|
|
||||||
|
|
||||||
### Restore
|
The system supports the following file formats:
|
||||||
|
|
||||||
```yml
|
- `.sql` (uncompressed SQL dump)
|
||||||
|
- `.sql.gz` (gzip-compressed SQL dump)
|
||||||
|
- `.sql.gpg` (GPG-encrypted SQL dump)
|
||||||
|
- `.sql.gz.gpg` (GPG-encrypted and gzip-compressed SQL dump)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Steps
|
||||||
|
|
||||||
|
1. **Specify the Backup File**: Use the `--file` flag to specify the backup file to restore.
|
||||||
|
2. **Provide Database Credentials**: Ensure the correct database connection details are provided.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Restore Configuration
|
||||||
|
|
||||||
|
Below is an example `docker-compose.yml` configuration for restoring a database:
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
# In production, it is advised to lock your image tag to a proper
|
# In production, lock your image tag to a specific release version
|
||||||
# release version instead of using `latest`.
|
# instead of using `latest`. Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
# for available releases.
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: restore -d database -f store_20231219_022941.sql.gz
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup restore -d database -f store_20231219_022941.sql.gz
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup # Mount the directory containing the backup file
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=postgres
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
# Ensure the mysql-bkup container is connected to the same network as your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web:
|
web:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Supported File Formats**: The restore process supports `.sql`, `.sql.gz`, `.sql.gpg`, and `.sql.gz.gpg` files.
|
||||||
|
- **Encrypted Backups**: If the backup is encrypted with GPG, ensure the `GPG_PASSPHRASE` environment variable is set for automatic decryption.
|
||||||
|
- **Network Configuration**: Ensure the `mysql-bkup` container is connected to the same network as your database.
|
||||||
+67
-73
@@ -6,98 +6,92 @@ nav_order: 1
|
|||||||
|
|
||||||
# About mysql-bkup
|
# About mysql-bkup
|
||||||
{:.no_toc}
|
{:.no_toc}
|
||||||
mysql-bkup is a Docker container image that can be used to backup and restore MySQL database. It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, and SSH compatible storage.
|
|
||||||
It also supports __encrypting__ your backups using GPG.
|
|
||||||
|
|
||||||
We are open to receiving stars, PRs, and issues!
|
**MYSQL-BKUP** is a Docker container image designed to **backup, restore, and migrate MySQL databases**.
|
||||||
|
It supports a variety of storage options and ensures data security through GPG encryption.
|
||||||
|
|
||||||
|
**MYSQL-BKUP** is designed for seamless deployment on **Docker** and **Kubernetes**, simplifying MySQL backup, restoration, and migration across environments.
|
||||||
|
It is a lightweight, multi-architecture solution compatible with **Docker**, **Docker Swarm**, **Kubernetes**, and other container orchestration platforms.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### Storage Options
|
||||||
|
- **Local storage**
|
||||||
|
- **AWS S3** or any S3-compatible object storage
|
||||||
|
- **FTP**
|
||||||
|
- **SFTP**
|
||||||
|
- **SSH-compatible storage**
|
||||||
|
- **Azure Blob storage**
|
||||||
|
|
||||||
|
### Data Security
|
||||||
|
- Backups can be encrypted using **GPG** to ensure data confidentiality.
|
||||||
|
|
||||||
|
### Deployment Flexibility
|
||||||
|
- Available as the [jkaninda/pg-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image.
|
||||||
|
- Deployable on **Docker**, **Docker Swarm**, and **Kubernetes**.
|
||||||
|
- Supports recurring backups of MySQL databases:
|
||||||
|
- On Docker for automated backup schedules.
|
||||||
|
- As a **Job** or **CronJob** on Kubernetes.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- Receive real-time updates on backup success or failure via:
|
||||||
|
- **Telegram**
|
||||||
|
- **Email**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡Use Cases
|
||||||
|
|
||||||
|
- **Scheduled Backups**: Automate recurring backups using Docker or Kubernetes.
|
||||||
|
- **Disaster Recovery:** Quickly restore backups to a clean MySQL instance.
|
||||||
|
- **Database Migration**: Seamlessly move data across environments using the built-in `migrate` feature.
|
||||||
|
- **Secure Archiving:** Keep backups encrypted and safely stored in the cloud or remote servers.
|
||||||
|
|
||||||
|
|
||||||
|
## ✅ Verified Platforms:
|
||||||
|
MYSQL-BKUP has been tested and runs successfully on:
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- Docker Swarm
|
||||||
|
- Kubernetes
|
||||||
|
- OpenShift
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Get Involved
|
||||||
|
|
||||||
|
We welcome contributions! Feel free to give us a ⭐, submit PRs, or open issues on our [GitHub repository](https://github.com/jkaninda/mysql-bkup).
|
||||||
|
|
||||||
{: .fs-6 .fw-300 }
|
{: .fs-6 .fw-300 }
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
The [jkaninda/mysql-bkup](https://hub.docker.com/r/jkaninda/mysql-bkup) Docker image can be deployed on Docker, Docker Swarm and Kubernetes.
|
|
||||||
It handles __recurring__ backups of postgres database on Docker and can be deployed as __CronJob on Kubernetes__ using local, AWS S3 or SSH compatible storage.
|
|
||||||
|
|
||||||
It also supports __encrypting__ your backups using GPG.
|
|
||||||
|
|
||||||
{: .note }
|
{: .note }
|
||||||
Code and documentation for `v1` version on [this branch][v1-branch].
|
Code and documentation for the `v1` version are available on [this branch][v1-branch].
|
||||||
|
|
||||||
[v1-branch]: https://github.com/jkaninda/mysql-bkup
|
[v1-branch]: https://github.com/jkaninda/mysql-bkup
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quickstart
|
## Available Image Registries
|
||||||
|
|
||||||
### Simple backup using Docker CLI
|
The Docker image is published to both **Docker Hub** and the **GitHub Container Registry**. You can use either of the following:
|
||||||
|
|
||||||
To run a one time backup, bind your local volume to `/backup` in the container and run the `mysql-bkup backup` command:
|
```bash
|
||||||
|
docker pull jkaninda/mysql-bkup
|
||||||
```shell
|
docker pull ghcr.io/jkaninda/mysql-bkup
|
||||||
docker run --rm --network your_network_name \
|
|
||||||
-v $PWD/backup:/backup/ \
|
|
||||||
-e "DB_HOST=dbhost" \
|
|
||||||
-e "DB_USERNAME=username" \
|
|
||||||
-e "DB_PASSWORD=password" \
|
|
||||||
jkaninda/mysql-bkup mysql-bkup backup -d database_name
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, pass a `--env-file` in order to use a full config as described below.
|
While the documentation references Docker Hub, all examples work seamlessly with `ghcr.io`.
|
||||||
|
|
||||||
### Simple backup in docker compose file
|
---
|
||||||
|
|
||||||
```yaml
|
|
||||||
services:
|
|
||||||
mysql-bkup:
|
|
||||||
# In production, it is advised to lock your image tag to a proper
|
|
||||||
# release version instead of using `latest`.
|
|
||||||
# Check https://github.com/jkaninda/mysql-bkup/releases
|
|
||||||
# for a list of available releases.
|
|
||||||
image: jkaninda/mysql-bkup
|
|
||||||
container_name: mysql-bkup
|
|
||||||
command:
|
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup
|
|
||||||
volumes:
|
|
||||||
- ./backup:/backup
|
|
||||||
environment:
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_HOST=postgres
|
|
||||||
- DB_NAME=foo
|
|
||||||
- DB_USERNAME=bar
|
|
||||||
- DB_PASSWORD=password
|
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
|
||||||
networks:
|
|
||||||
- web
|
|
||||||
networks:
|
|
||||||
web:
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available image registries
|
|
||||||
|
|
||||||
This Docker image is published to both Docker Hub and the GitHub container registry.
|
|
||||||
Depending on your preferences and needs, you can reference both `jkaninda/mysql-bkup` as well as `ghcr.io/jkaninda/mysql-bkup`:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker pull jkaninda/mysql-bkup:v1.0
|
|
||||||
docker pull ghcr.io/jkaninda/mysql-bkup:v1.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Documentation references Docker Hub, but all examples will work using ghcr.io just as well.
|
|
||||||
|
|
||||||
## Supported Engines
|
|
||||||
|
|
||||||
This image is developed and tested against the Docker CE engine and Kubernetes exclusively.
|
|
||||||
While it may work against different implementations, there are no guarantees about support for non-Docker engines.
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
We decided to publish this image as a simpler and more lightweight alternative because of the following requirements:
|
We created this image as a simpler and more lightweight alternative to existing solutions. Here’s why:
|
||||||
|
|
||||||
- The original image is based on `ubuntu` and requires additional tools, making it heavy.
|
- **Lightweight:** Written in Go, the image is optimized for performance and minimal resource usage.
|
||||||
- This image is written in Go.
|
- **Multi-Architecture Support:** Supports `arm64` and `arm/v7` architectures.
|
||||||
- `arm64` and `arm/v7` architectures are supported.
|
- **Docker Swarm Support:** Fully compatible with Docker in Swarm mode.
|
||||||
- Docker in Swarm mode is supported.
|
- **Kubernetes Support:** Designed to work seamlessly with Kubernetes.
|
||||||
- Kubernetes is supported.
|
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
title: Quickstart
|
||||||
|
layout: home
|
||||||
|
nav_order: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quickstart
|
||||||
|
|
||||||
|
This guide provides quick examples for running backups using Docker CLI, Docker Compose, and Kubernetes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Simple Backup Using Docker CLI
|
||||||
|
|
||||||
|
To perform a one-time backup, bind your local volume to `/backup` in the container and run the `backup` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
-e "DB_HOST=dbhost" \
|
||||||
|
-e "DB_PORT=3306" \
|
||||||
|
-e "DB_USER=username" \
|
||||||
|
-e "DB_PASSWORD=password" \
|
||||||
|
jkaninda/mysql-bkup backup -d database_name
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, use an environment file (`--env-file`) for configuration:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
--env-file your-env-file \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
jkaninda/mysql-bkup backup -d database_name
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup All Databases
|
||||||
|
|
||||||
|
To back up all databases on the server, use the `--all-databases` or `-a` flag. By default, this creates individual backup files for each database.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
-e "DB_HOST=dbhost" \
|
||||||
|
-e "DB_PORT=3306" \
|
||||||
|
-e "DB_USER=username" \
|
||||||
|
-e "DB_PASSWORD=password" \
|
||||||
|
jkaninda/mysql-bkup backup --all-databases --disable-compression
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Use the `--all-in-one` or `-A` flag to combine backups into a single file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Simple Restore Using Docker CLI
|
||||||
|
|
||||||
|
To restore a database, bind your local volume to `/backup` and run the `restore` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm --network your_network_name \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
-e "DB_HOST=dbhost" \
|
||||||
|
-e "DB_PORT=3306" \
|
||||||
|
-e "DB_USER=username" \
|
||||||
|
-e "DB_PASSWORD=password" \
|
||||||
|
jkaninda/mysql-bkup restore -d database_name -f backup_file.sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Backup with Docker Compose
|
||||||
|
|
||||||
|
Below is an example of a `docker-compose.yml` file for running a one-time backup:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
mysql-bkup:
|
||||||
|
# In production, pin your image tag to a specific release version instead of `latest`.
|
||||||
|
# See available releases: https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
container_name: mysql-bkup
|
||||||
|
command: backup
|
||||||
|
volumes:
|
||||||
|
- ./backup:/backup
|
||||||
|
environment:
|
||||||
|
- DB_PORT=3306
|
||||||
|
- DB_HOST=mysql
|
||||||
|
- DB_NAME=foo
|
||||||
|
- DB_USER=bar
|
||||||
|
- DB_PASSWORD=password
|
||||||
|
- TZ=Europe/Paris
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Recurring Backups with Docker
|
||||||
|
|
||||||
|
You can schedule recurring backups using the `--cron-expression` or `-e` flag:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --rm --network network_name \
|
||||||
|
-v $PWD/backup:/backup/ \
|
||||||
|
-e "DB_HOST=hostname" \
|
||||||
|
-e "DB_USER=user" \
|
||||||
|
-e "DB_PASSWORD=password" \
|
||||||
|
jkaninda/mysql-bkup backup -d dbName --cron-expression "@every 15m"
|
||||||
|
```
|
||||||
|
|
||||||
|
For predefined schedules, refer to the [documentation](https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deploy on Kubernetes
|
||||||
|
|
||||||
|
For Kubernetes, you can deploy `mysql-bkup` as a Job or CronJob. Below are examples for both.
|
||||||
|
|
||||||
|
### Kubernetes Backup Job
|
||||||
|
|
||||||
|
This example defines a one-time backup job:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: backup-job
|
||||||
|
spec:
|
||||||
|
ttlSecondsAfterFinished: 100
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mysql-bkup
|
||||||
|
# Pin the image tag to a specific release version in production.
|
||||||
|
# See available releases: https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- backup -d dbname
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
env:
|
||||||
|
- name: DB_HOST
|
||||||
|
value: "mysql"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "user"
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: "password"
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /backup
|
||||||
|
name: backup
|
||||||
|
volumes:
|
||||||
|
- name: backup
|
||||||
|
hostPath:
|
||||||
|
path: /home/toto/backup # Directory location on the host
|
||||||
|
type: Directory # Optional field
|
||||||
|
restartPolicy: Never
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kubernetes CronJob for Scheduled Backups
|
||||||
|
|
||||||
|
For scheduled backups, use a `CronJob`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: CronJob
|
||||||
|
metadata:
|
||||||
|
name: pg-bkup-cronjob
|
||||||
|
spec:
|
||||||
|
schedule: "0 2 * * *" # Runs daily at 2 AM
|
||||||
|
jobTemplate:
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: pg-bkup
|
||||||
|
image: jkaninda/mysql-bkup
|
||||||
|
command:
|
||||||
|
- /bin/sh
|
||||||
|
- -c
|
||||||
|
- backup -d dbname
|
||||||
|
env:
|
||||||
|
- name: DB_HOST
|
||||||
|
value: "mysql"
|
||||||
|
- name: DB_USER
|
||||||
|
value: "user"
|
||||||
|
- name: DB_PASSWORD
|
||||||
|
value: "password"
|
||||||
|
volumeMounts:
|
||||||
|
- mountPath: /backup
|
||||||
|
name: backup
|
||||||
|
volumes:
|
||||||
|
- name: backup
|
||||||
|
hostPath:
|
||||||
|
path: /home/toto/backup
|
||||||
|
type: Directory
|
||||||
|
restartPolicy: OnFailure
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Notes
|
||||||
|
|
||||||
|
- **Volume Binding**: Ensure the `/backup` directory is mounted to persist backup files.
|
||||||
|
- **Environment Variables**: Use environment variables or an `--env-file` to pass database credentials and other configurations.
|
||||||
|
- **Cron Expressions**: Use standard cron expressions or predefined schedules for recurring backups.
|
||||||
|
- **Kubernetes Jobs**: Use Kubernetes `Job` or `CronJob` for running backups in a Kubernetes cluster.
|
||||||
+273
-77
@@ -1,105 +1,301 @@
|
|||||||
---
|
---
|
||||||
title: Configuration Reference
|
title: Configuration Reference
|
||||||
layout: default
|
layout: default
|
||||||
nav_order: 2
|
nav_order: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
# Configuration reference
|
# Configuration Reference
|
||||||
|
|
||||||
Backup and restore targets, schedule and retention are configured using environment variables or flags.
|
mysql-bkup is configured with **environment variables**, **CLI flags** and an optional **YAML file**. For every setting the order is: flag, environment variable, deprecated name, YAML `env:` section, default. Every secret (`DB_PASSWORD`, `AWS_SECRET_KEY`, `GPG_PASSPHRASE`, ...) can also be read from a file with `<NAME>_FILE`.
|
||||||
|
|
||||||
|
Run `mysql-bkup config` to see the resolved configuration and where each value comes from (secrets redacted).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---|---|
|
||||||
|
| `backup` | Back up now, or as a daemon with `--cron-expression` |
|
||||||
|
| `restore` | Restore a backup: `-f FILE [-d DB]` |
|
||||||
|
| `migrate` | Copy a database (or every database with `-a`) into `TARGET_DB_*`, streamed |
|
||||||
|
| `verify` | Download, decrypt and decompress a backup and check its sha256 |
|
||||||
|
| `list` | Backup history from the index (`--json`, `--storage-scan`) |
|
||||||
|
| `delete` | Delete a backup, its sidecar and its index entry (`-f FILE`) |
|
||||||
|
| `prune` | Apply the retention policy (`--dry-run`) |
|
||||||
|
| `reconcile` | Rebuild the backup index (`--from-index OLD`, `--dry-run`) |
|
||||||
|
| `config` | Show the resolved configuration; `--env-docs` prints the tables below |
|
||||||
|
| `health` | Exit 0 when the daemon is healthy (Docker `HEALTHCHECK`) |
|
||||||
|
| `version` | Print the version |
|
||||||
|
|
||||||
|
## Flags
|
||||||
|
|
||||||
### CLI utility Usage
|
| Flag | Short | Commands | Variable |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `--dbname` | `-d` | all | `DB_NAME` |
|
||||||
|
| `--storage` | `-s` | all | `STORAGE` (`local`, `s3`, `gcs`, `azure`, `sftp`/`ssh`/`remote`, `ftp`) |
|
||||||
|
| `--path` (`--remote-path`) | `-P` | all | `REMOTE_PATH` |
|
||||||
|
| `--file` | `-f` | restore, verify, delete | `FILE_NAME` |
|
||||||
|
| `--config` | `-c` | backup, list, prune, config | `BACKUP_CONFIG_FILE` |
|
||||||
|
| `--cron-expression` | `-e` | backup | `BACKUP_CRON_EXPRESSION` |
|
||||||
|
| `--all-databases` | `-a` | backup, migrate | `BACKUP_ALL_DATABASES` |
|
||||||
|
| `--all-in-one` | `-A` | backup | `BACKUP_ALL_IN_ONE` |
|
||||||
|
| `--exclude-db` | | backup | `BACKUP_EXCLUDE_DATABASES` |
|
||||||
|
| `--custom-name` | | backup | `BACKUP_CUSTOM_NAME` (fixed name, no timestamp) |
|
||||||
|
| `--name` | `-n` | backup, list, prune | `BACKUP_NAME` |
|
||||||
|
| `--disable-compression` | | backup | `DISABLE_COMPRESSION` |
|
||||||
|
| `--compression` | | backup | `COMPRESSION` (`gzip`, `zstd`, `none`) |
|
||||||
|
| `--encryption` | | backup | `ENCRYPTION` (`auto`, `none`, `gpg`, `age`) |
|
||||||
|
| `--index-name` | | all | `BACKUP_INDEX_NAME` |
|
||||||
|
| `--result-file` | | all | `RESULT_FILE` |
|
||||||
|
| `--help` | `-h` | all | |
|
||||||
|
| `--version` | `-v` | root | |
|
||||||
|
|
||||||
| Options | Shorts | Usage |
|
## Exit codes
|
||||||
|-----------------------|--------|----------------------------------------------------------------------------------------|
|
|
||||||
| mysql-bkup | bkup | CLI utility |
|
| Code | Meaning |
|
||||||
| backup | | Backup database operation |
|
|---|---|
|
||||||
| restore | | Restore database operation |
|
| `0` | success |
|
||||||
| --storage | -s | Storage. local or s3 (default: local) |
|
| `1` | failure |
|
||||||
| --file | -f | File name for restoration |
|
| `2` | configuration error |
|
||||||
| --path | | AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup` |
|
| `3` | database connection or authentication |
|
||||||
| --dbname | -d | Database name |
|
| `4` | storage |
|
||||||
| --port | -p | Database port (default: 3306) |
|
| `5` | encryption or decryption |
|
||||||
| --mode | -m | Execution mode. default or scheduled (default: default) |
|
|
||||||
| --disable-compression | | Disable database backup compression |
|
|
||||||
| --prune | | Delete old backup, default disabled |
|
|
||||||
| --keep-last | | Delete old backup created more than specified days ago, default 7 days |
|
|
||||||
| --period | | Crontab period for scheduled mode only. (default: "0 1 * * *") |
|
|
||||||
| --help | -h | Print this help message and exit |
|
|
||||||
| --version | -V | Print version information and exit |
|
|
||||||
|
|
||||||
## Environment variables
|
## Environment variables
|
||||||
|
|
||||||
| Name | Requirement | Description |
|
### storage
|
||||||
|-------------------|--------------------------------------------------|------------------------------------------------------|
|
|
||||||
| DB_PORT | Optional, default 3306 | Database port number |
|
|
||||||
| DB_HOST | Required | Database host |
|
|
||||||
| DB_NAME | Optional if it was provided from the -d flag | Database name |
|
|
||||||
| DB_USERNAME | Required | Database user name |
|
|
||||||
| DB_PASSWORD | Required | Database password |
|
|
||||||
| AWS_ACCESS_KEY | Optional, required for S3 storage | AWS S3 Access Key |
|
|
||||||
| AWS_SECRET_KEY | Optional, required for S3 storage | AWS S3 Secret Key |
|
|
||||||
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
|
||||||
| AWS_BUCKET_NAME | Optional, required for S3 storage | AWS S3 Bucket Name |
|
|
||||||
| AWS_REGION | Optional, required for S3 storage | AWS Region |
|
|
||||||
| AWS_DISABLE_SSL | Optional, required for S3 storage | Disable SSL |
|
|
||||||
| FILE_NAME | Optional if it was provided from the --file flag | Database file to restore (extensions: .sql, .sql.gz) |
|
|
||||||
| Gmysql_PASSPHRASE | Optional, required to encrypt and restore backup | Gmysql passphrase |
|
|
||||||
| SSH_HOST_NAME | Optional, required for SSH storage | ssh remote hostname or ip |
|
|
||||||
| SSH_USER | Optional, required for SSH storage | ssh remote user |
|
|
||||||
| SSH_PASSWORD | Optional, required for SSH storage | ssh remote user's password |
|
|
||||||
| SSH_IDENTIFY_FILE | Optional, required for SSH storage | ssh remote user's private key |
|
|
||||||
| SSH_PORT | Optional, required for SSH storage | ssh remote server port |
|
|
||||||
| SSH_REMOTE_PATH | Optional, required for SSH storage | ssh remote path (/home/toto/backup) |
|
|
||||||
|
|
||||||
---
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
## Run in Scheduled mode
|
|---|---|---|---|---|
|
||||||
|
| `STORAGE` | `--storage` `-s` | `local` | | Storage backend: local, s3, gcs, azure, sftp (ssh, remote), ftp (local, s3, gcs, azure, sftp, ssh, ftp, remote) |
|
||||||
|
| `STORAGE_PATH` | | `/backup` | | Root directory of the local storage backend |
|
||||||
|
| `REMOTE_PATH` | `--path` `-P` | | `AWS_S3_PATH` (deprecated), `S3_PATH` (deprecated) | Folder inside the storage backend (without file name) |
|
||||||
|
|
||||||
This image can be run as CronJob in Kubernetes for a regular backup which makes deployment on Kubernetes easy as Kubernetes has CronJob resources.
|
### general
|
||||||
For Docker, you need to run it in scheduled mode by adding `--mode scheduled` flag and specify the periodical backup time by adding `--period "0 1 * * *"` flag.
|
|
||||||
|
|
||||||
## Syntax of crontab (field description)
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `BACKUP_NAME` | `--name` `-n` | | | Base name of the artifact; a timestamp is appended (default: the database or volume name) |
|
||||||
|
| `BACKUP_CUSTOM_NAME` | `--custom-name` | | | Fixed artifact name without timestamp (overwrites the previous one) |
|
||||||
|
| `FILE_NAME` | `--file` `-f` | | | Backup file to restore, verify or delete |
|
||||||
|
| `BACKUP_CONFIG_FILE` | `--config` `-c` | | | YAML configuration file (default /config/config.yaml if present) |
|
||||||
|
| `BACKUP_TIMEOUT` | | | | Abort a backup after this duration (0 = none) |
|
||||||
|
| `RESTORE_TIMEOUT` | | | | Abort a restore after this duration (0 = none) |
|
||||||
|
| `TMP_DIR` | | `/tmp/backup` | `BACKUP_TMP_DIR` (deprecated) | Scratch directory (only for secret files and disk-based modes) |
|
||||||
|
| `COMPRESSION` | `--compression` | `gzip` | | Compression: gzip, zstd or none (gzip, zstd, none) |
|
||||||
|
| `COMPRESSION_LEVEL` | | | | Compression level (0 = default) |
|
||||||
|
| `DISABLE_COMPRESSION` | `--disable-compression` | | | Same as COMPRESSION=none |
|
||||||
|
| `RESTORE_VERIFY_FIRST` | `--verify-first` | `auto` | | Verify the whole artifact before restoring (auto: on for GPG-encrypted volumes) (auto, true, false) |
|
||||||
|
| `LOG_LEVEL` | | `info` | | Log level (debug, info, warn, error) |
|
||||||
|
| `LOG_FORMAT` | | `text` | | Log format (text, json) |
|
||||||
|
| `TIME_FORMAT` | | `2006-01-02 15:04:05` | | Time layout used in notifications |
|
||||||
|
| `RESULT_FILE` | `--result-file` | | | Write the run result as JSON to this path |
|
||||||
|
|
||||||
The syntax is:
|
### scheduling
|
||||||
|
|
||||||
- 1: Minute (0-59)
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
- 2: Hours (0-23)
|
|---|---|---|---|---|
|
||||||
- 3: Day (0-31)
|
| `BACKUP_CRON_EXPRESSION` | `--cron-expression` `-e` | | | Run as a daemon on this cron schedule (5 or 6 fields, @daily, @every 6h) |
|
||||||
- 4: Month (0-12 [12 == December])
|
| `BACKUP_RUN_ON_START` | | | | In daemon mode, run one backup immediately at start |
|
||||||
- 5: Day of the week(0-7 [7 or 0 == sunday])
|
| `BACKUP_RESCUE_MODE` | | | | Deprecated: runs always continue with the remaining targets after a failure |
|
||||||
|
| `METRICS_ADDR` | | | | Serve Prometheus /metrics and /healthz on this address in daemon mode (e.g. :9090) |
|
||||||
|
| `HEALTH_FILE` | | `/tmp/bkup-health.json` | | State file read by the health command |
|
||||||
|
| `HEALTH_MAX_AGE` | | | | health fails when the last success is older than this (default: 2x the schedule interval) |
|
||||||
|
|
||||||
Easy to remember format:
|
### retention
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `BACKUP_RETENTION_DAYS` | | `0` | | Delete backups older than N days (0 = keep) |
|
||||||
|
| `BACKUP_RETENTION_COUNT` | | `0` | | Keep only the newest N backups (0 = no limit) |
|
||||||
|
| `BACKUP_RETENTION_ALLOW_ROOT` | | | | Allow pattern-based retention at the storage root (index disabled) |
|
||||||
|
| `BACKUP_RETENTION_ADOPT_LEGACY` | | | | Import pre-index backups matching this tool's naming into the index |
|
||||||
|
| `BACKUP_INDEX` | | `true` | | Keep the backup index file and per-backup .meta.json sidecars |
|
||||||
|
| `BACKUP_INDEX_NAME` | `--index-name` | `{tool}-metadata.json` | | Index file name; placeholders {tool} {engine} {name} {db} |
|
||||||
|
|
||||||
|
### storage-s3
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `AWS_S3_ENDPOINT` | | | `S3_ENDPOINT` (deprecated), `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL` | S3 endpoint (empty = AWS) |
|
||||||
|
| `AWS_S3_BUCKET_NAME` | | | `BUCKET_NAME` (deprecated), `AWS_BUCKET_NAME` (deprecated) | S3 bucket |
|
||||||
|
| `AWS_REGION` | | `us-east-1` | `AWS_DEFAULT_REGION` | S3 region |
|
||||||
|
| `AWS_ACCESS_KEY` | | | `ACCESS_KEY` (deprecated), `AWS_ACCESS_KEY_ID` | S3 access key (empty = default AWS credential chain) |
|
||||||
|
| `AWS_SECRET_KEY` | | | `SECRET_KEY` (deprecated), `AWS_SECRET_ACCESS_KEY`, `AWS_SECRET_KEY_FILE` | S3 secret key |
|
||||||
|
| `AWS_SESSION_TOKEN` | | | `AWS_SESSION_TOKEN_FILE` | S3 session token |
|
||||||
|
| `AWS_PROFILE` | | | | AWS shared config profile |
|
||||||
|
| `AWS_DISABLE_SSL` | | | | Use http for the S3 endpoint |
|
||||||
|
| `AWS_FORCE_PATH_STYLE` | | | | Path-style S3 addressing (MinIO) |
|
||||||
|
| `S3_STORAGE_CLASS` | | | | S3 storage class |
|
||||||
|
| `S3_SSE` | | | | Server-side encryption: AES256 or aws:kms (aes256, aws:kms) |
|
||||||
|
| `S3_SSE_KMS_KEY_ID` | | | | KMS key id for aws:kms |
|
||||||
|
| `S3_PART_SIZE` | | `64MiB` | | Multipart part size; max object size is 10000 x this |
|
||||||
|
| `S3_UPLOAD_CONCURRENCY` | | `2` | | Parallel part uploads (memory = part size x this) |
|
||||||
|
|
||||||
|
### storage-sftp
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `SFTP_HOST` | | | | SFTP host |
|
||||||
|
| `SFTP_PORT` | | `22` | | SFTP port |
|
||||||
|
| `SFTP_USER` | | | | SFTP user |
|
||||||
|
| `SFTP_PASSWORD` | | | `SFTP_PASSWORD_FILE` | SFTP password |
|
||||||
|
| `SFTP_IDENTITY_FILE` | | | | SFTP private key file (the SSH agent is also used when SSH_AUTH_SOCK is set) |
|
||||||
|
| `SFTP_IDENTITY_PASSPHRASE` | | | `SFTP_IDENTITY_PASSPHRASE_FILE` | Passphrase of the SFTP private key |
|
||||||
|
| `SFTP_KNOWN_HOSTS` | | | | known_hosts file used to verify the SFTP host key (default ~/.ssh/known_hosts) |
|
||||||
|
| `SFTP_INSECURE_IGNORE_HOST_KEY` | | | | Do not verify the SFTP host key (insecure) |
|
||||||
|
|
||||||
|
### storage-ftp
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `FTP_HOST` | | | `FTP_HOST_NAME` (deprecated) | FTP host |
|
||||||
|
| `FTP_PORT` | | `21` | | FTP port |
|
||||||
|
| `FTP_USER` | | | | FTP user |
|
||||||
|
| `FTP_PASSWORD` | | | `FTP_PASSWORD_FILE` | FTP password |
|
||||||
|
| `FTP_TLS` | | | | Use explicit FTPS |
|
||||||
|
|
||||||
|
### storage-azure
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `AZURE_STORAGE_ACCOUNT_NAME` | | | | Azure storage account |
|
||||||
|
| `AZURE_STORAGE_ACCOUNT_KEY` | | | `AZURE_STORAGE_ACCOUNT_KEY_FILE` | Azure storage account key |
|
||||||
|
| `AZURE_STORAGE_CONTAINER_NAME` | | | | Azure blob container |
|
||||||
|
| `AZURE_STORAGE_ENDPOINT` | | | | Azure blob endpoint (Azurite, sovereign clouds) |
|
||||||
|
| `AZURE_STORAGE_SAS_TOKEN` | | | `AZURE_STORAGE_SAS_TOKEN_FILE` | Azure SAS token (instead of the account key) |
|
||||||
|
|
||||||
|
### storage-gcs
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `GCS_BUCKET` | | | | GCS bucket |
|
||||||
|
| `GCS_CREDENTIALS_FILE` | | | `GOOGLE_APPLICATION_CREDENTIALS` | GCS service account JSON file |
|
||||||
|
| `GCS_CREDENTIALS_JSON` | | | `GCS_CREDENTIALS_JSON_FILE` | GCS service account JSON content |
|
||||||
|
| `GCS_ENDPOINT` | | | | GCS endpoint (emulators) |
|
||||||
|
|
||||||
|
### encryption
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `ENCRYPTION` | `--encryption` | `auto` | | auto (from the keys set), none, gpg or age (auto, none, gpg, age) |
|
||||||
|
| `ENCRYPTION_ARMOR` | | | | ASCII-armor encrypted output |
|
||||||
|
| `GPG_PASSPHRASE` | | | `GPG_PASSPHRASE_FILE` | GPG symmetric passphrase |
|
||||||
|
| `GPG_PUBLIC_KEY` | | | | GPG public key(s): file path or inline key (default /config/gnupg/public_key.{asc,gpg}) |
|
||||||
|
| `GPG_PRIVATE_KEY` | | | `GPG_PRIVATE_KEY_FILE` | GPG private key for restore: file path or inline key (default /config/gnupg/private_key.{asc,gpg}) |
|
||||||
|
| `GPG_PRIVATE_KEY_PASSPHRASE` | | | `GPG_PRIVATE_KEY_PASSPHRASE_FILE` | Passphrase of the GPG private key (default GPG_PASSPHRASE) |
|
||||||
|
| `AGE_RECIPIENTS` | | | | age recipients (age1..., ssh-ed25519 ...), comma or newline separated |
|
||||||
|
| `AGE_RECIPIENTS_FILE` | | | | File with age recipients |
|
||||||
|
| `AGE_PASSPHRASE` | | | `AGE_PASSPHRASE_FILE` | age passphrase (scrypt) |
|
||||||
|
| `AGE_IDENTITY` | | | `AGE_IDENTITY_FILE` | age identity (AGE-SECRET-KEY-...) for restore |
|
||||||
|
| `AGE_IDENTITY_FILE` | | | | age identity file for restore (default /config/age/identity.txt) |
|
||||||
|
|
||||||
|
### notifications
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `MAIL_HOST` | | | | SMTP host |
|
||||||
|
| `MAIL_PORT` | | `587` | | SMTP port |
|
||||||
|
| `MAIL_USERNAME` | | | | SMTP user |
|
||||||
|
| `MAIL_PASSWORD` | | | `MAIL_PASSWORD_FILE` | SMTP password |
|
||||||
|
| `MAIL_FROM` | | | | Sender address |
|
||||||
|
| `MAIL_TO` | | | | Recipients, comma separated |
|
||||||
|
| `MAIL_SKIP_TLS` | | | | Deprecated: true skips certificate verification (use MAIL_INSECURE_SKIP_VERIFY) |
|
||||||
|
| `MAIL_INSECURE_SKIP_VERIFY` | | | | Skip SMTP certificate verification |
|
||||||
|
| `MAIL_TLS` | | `starttls` | | SMTP TLS mode (starttls, tls, none) |
|
||||||
|
| `TG_TOKEN` | | | `TELEGRAM_BOT_TOKEN`, `TG_TOKEN_FILE` | Telegram bot token |
|
||||||
|
| `TG_CHAT_ID` | | | `TELEGRAM_CHAT_ID` | Telegram chat id |
|
||||||
|
| `POSTA_URL` | | | | Posta base URL (https://posta.example.com) for email notifications through Posta |
|
||||||
|
| `POSTA_API_KEY` | | | `POSTA_API_KEY_FILE` | Posta API key with the send scope |
|
||||||
|
| `POSTA_WORKSPACE_ID` | | | | Posta workspace id (only for account-wide API keys) |
|
||||||
|
| `POSTA_FROM` | | | | Posta sender address (default MAIL_FROM) |
|
||||||
|
| `POSTA_TO` | | | | Posta recipients, comma separated (default MAIL_TO) |
|
||||||
|
| `POSTA_TEMPLATE` | | | | Posta template name for notifications; the event fields are passed as template data (default: built-in HTML) |
|
||||||
|
| `POSTA_TEMPLATE_ERROR` | | | | Posta template name for failures (default POSTA_TEMPLATE) |
|
||||||
|
| `POSTA_LANGUAGE` | | | | Posta template language |
|
||||||
|
| `WEBHOOK_URL` | | | | Generic JSON webhook URL |
|
||||||
|
| `WEBHOOK_SECRET` | | | `WEBHOOK_SECRET_FILE` | HMAC-SHA256 secret for the X-Bkup-Signature header |
|
||||||
|
| `SLACK_WEBHOOK_URL` | | | `SLACK_WEBHOOK_URL_FILE` | Slack incoming webhook URL |
|
||||||
|
| `DISCORD_WEBHOOK_URL` | | | `DISCORD_WEBHOOK_URL_FILE` | Discord webhook URL |
|
||||||
|
| `NTFY_URL` | | | | ntfy topic URL |
|
||||||
|
| `NTFY_TOKEN` | | | `NTFY_TOKEN_FILE` | ntfy access token |
|
||||||
|
| `NOTIFY_ON` | | `always` | | When to notify (always, failure, success, never) |
|
||||||
|
| `HEALTHCHECK_PING_URL` | | | | healthchecks.io-style URL pinged on start (/start), success and failure (/fail) |
|
||||||
|
| `BACKUP_REFERENCE` | | | | Free text included in notifications |
|
||||||
|
| `TEMPLATES_DIR` | | `/config/templates` | | Directory with custom notification templates |
|
||||||
|
|
||||||
|
### database
|
||||||
|
|
||||||
|
| Variable | Flag | Default | Fallbacks | Description |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `DB_URL` | | | `DB_URL_FILE` | Connection URL mysql://user:password@host:port/database?ssl-mode=required (overrides host, port, user, password) |
|
||||||
|
| `DB_HOST` | | `localhost` | | Database host (a path starting with / is a Unix socket) |
|
||||||
|
| `DB_PORT` | | `3306` | | Database port |
|
||||||
|
| `DB_NAME` | `--dbname` `-d` | | | Database name |
|
||||||
|
| `DB_USER` | | | `DB_USERNAME` | Database user |
|
||||||
|
| `DB_PASSWORD` | | | `DB_PASSWORD_FILE` | Database password |
|
||||||
|
| `DB_SSL_MODE` | | `disabled` | | TLS mode: disabled, preferred, required, verify_ca, verify_identity (1.x values 0 and 1 mean disabled and required) |
|
||||||
|
| `DB_SSL_CA` | | `/etc/ssl/certs/ca-certificates.crt` | | CA certificate file, used with DB_SSL_MODE=verify_ca or verify_identity |
|
||||||
|
| `DB_SSL_CERT` | | | | Client certificate file |
|
||||||
|
| `DB_SSL_KEY` | | | | Client key file |
|
||||||
|
| `BACKUP_ALL_DATABASES` | `--all-databases` `-a` | | | Back up every database, one file each |
|
||||||
|
| `BACKUP_ALL_IN_ONE` | `--all-in-one` `-A` | | | Back up every database into a single file |
|
||||||
|
| `BACKUP_EXCLUDE_DATABASES` | `--exclude-db` | | | Databases to skip with --all-databases, comma separated |
|
||||||
|
| `TARGET_DB_URL` | | | `TARGET_DB_URL_FILE` | migrate: target connection URL |
|
||||||
|
| `TARGET_DB_HOST` | | | | migrate: target host |
|
||||||
|
| `TARGET_DB_PORT` | | `3306` | | migrate: target port |
|
||||||
|
| `TARGET_DB_NAME` | | | | migrate: target database (default: the source name) |
|
||||||
|
| `TARGET_DB_USER` | | | `TARGET_DB_USERNAME` | migrate: target user |
|
||||||
|
| `TARGET_DB_PASSWORD` | | | `TARGET_DB_PASSWORD_FILE` | migrate: target password |
|
||||||
|
| `TARGET_DB_SSL_MODE` | | `disabled` | | migrate: target TLS mode (same values as DB_SSL_MODE) |
|
||||||
|
| `TARGET_DB_SSL_CA` | | `/etc/ssl/certs/ca-certificates.crt` | | migrate: target CA certificate file |
|
||||||
|
| `MYSQL_CLIENT` | | `auto` | | Client tools: auto (mariadb-dump for MariaDB, Oracle mysqldump for MySQL), mysql or mariadb (auto, mysql, mariadb) |
|
||||||
|
|
||||||
|
|
||||||
|
## Scheduled Backups
|
||||||
|
|
||||||
|
### Running in Scheduled Mode
|
||||||
|
|
||||||
|
- **Docker**: Use the `--cron-expression` flag or the `BACKUP_CRON_EXPRESSION` environment variable to schedule backups.
|
||||||
|
- **Kubernetes**: Use a `CronJob` resource for scheduled backups.
|
||||||
|
|
||||||
|
### Cron Syntax
|
||||||
|
|
||||||
|
The cron syntax consists of five fields (a sixth, leading seconds field is also accepted):
|
||||||
|
|
||||||
```conf
|
```conf
|
||||||
* * * * * command to be executed
|
* * * * * command
|
||||||
```
|
```
|
||||||
|
|
||||||
|
| Field | Description | Values |
|
||||||
|
|---------------|------------------------------|----------------|
|
||||||
|
| Minute | Minute of the hour | `0-59` |
|
||||||
|
| Hour | Hour of the day | `0-23` |
|
||||||
|
| Day of Month | Day of the month | `1-31` |
|
||||||
|
| Month | Month of the year | `1-12` |
|
||||||
|
| Day of Week | Day of the week (0 = Sunday) | `0-7` |
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
- **Every 30 minutes**: `*/30 * * * *`
|
||||||
|
- **Every hour at minute 0**: `0 * * * *`
|
||||||
|
- **Every day at 1:00 AM**: `0 1 * * *`
|
||||||
|
|
||||||
|
### Predefined Schedules
|
||||||
|
|
||||||
|
| Entry | Description | Equivalent To |
|
||||||
|
|----------------------------|--------------------------------------------|---------------|
|
||||||
|
| `@yearly` (or `@annually`) | Run once a year, midnight, Jan. 1st | `0 0 1 1 *` |
|
||||||
|
| `@monthly` | Run once a month, midnight, first of month | `0 0 1 * *` |
|
||||||
|
| `@weekly` | Run once a week, midnight between Sat/Sun | `0 0 * * 0` |
|
||||||
|
| `@daily` (or `@midnight`) | Run once a day, midnight | `0 0 * * *` |
|
||||||
|
| `@hourly` | Run once an hour, beginning of hour | `0 * * * *` |
|
||||||
|
|
||||||
|
### Intervals
|
||||||
|
|
||||||
|
You can also schedule backups at fixed intervals using the format:
|
||||||
|
|
||||||
```conf
|
```conf
|
||||||
- - - - -
|
@every <duration>
|
||||||
| | | | |
|
|
||||||
| | | | ----- Day of week (0 - 7) (Sunday=0 or 7)
|
|
||||||
| | | ------- Month (1 - 12)
|
|
||||||
| | --------- Day of month (1 - 31)
|
|
||||||
| ----------- Hour (0 - 23)
|
|
||||||
------------- Minute (0 - 59)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> At every 30th minute
|
- Example: `@every 1h30m10s` runs the backup every 1 hour, 30 minutes, and 10 seconds.
|
||||||
|
|
||||||
```conf
|
|
||||||
*/30 * * * *
|
|
||||||
```
|
|
||||||
> “At minute 0.” every hour
|
|
||||||
```conf
|
|
||||||
0 * * * *
|
|
||||||
```
|
|
||||||
|
|
||||||
> “At 01:00.” every day
|
|
||||||
|
|
||||||
```conf
|
|
||||||
0 1 * * *
|
|
||||||
```
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
title: Upgrading to v2
|
||||||
|
layout: default
|
||||||
|
nav_order: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
# Upgrading from 1.x to v2
|
||||||
|
|
||||||
|
v2 rebuilds mysql-bkup on a shared streaming core: dump, compression, encryption and upload run in one pipe, without temporary files, whatever the size of the database. Existing commands, flags and environment variables keep working.
|
||||||
|
|
||||||
|
|
||||||
|
Everything that worked in 1.x keeps working; a few behaviours changed on purpose.
|
||||||
|
|
||||||
|
- **Image:** Debian 13 slim instead of Alpine, with Oracle's `mysqldump` for MySQL servers (correct MySQL 8 dumps) and `mariadb-dump` for MariaDB. `/bin/sh`, `/backup`, `/config`, the `backup`/`restore`/`migrate` shortcuts and the `bkup` alias are still there; `tini` is now PID 1.
|
||||||
|
- **Renamed variables** are still read, with a deprecation warning: `S3_ENDPOINT` → `AWS_S3_ENDPOINT`, `BUCKET_NAME`/`AWS_BUCKET_NAME` → `AWS_S3_BUCKET_NAME`, `ACCESS_KEY`/`SECRET_KEY` → `AWS_ACCESS_KEY`/`AWS_SECRET_KEY`, `AWS_S3_PATH`/`S3_PATH` → `REMOTE_PATH`, `FTP_HOST_NAME` → `FTP_HOST`, `BACKUP_TMP_DIR` → `TMP_DIR`.
|
||||||
|
- **`DB_USER` is the database user variable** (also `TARGET_DB_USER`). `DB_USERNAME` and `TARGET_DB_USERNAME` are still read without a warning, including the per-database forms (`DB_USERNAME_<NAME>`, `<NAME>_DB_USERNAME`).
|
||||||
|
- **`DB_SSL_MODE`** takes MySQL's mode names; `0`/`1` still work.
|
||||||
|
- **`STORAGE` and `STORAGE_PATH` env are honoured** (1.x always used the `--storage` default and `/backup`). Unknown storage names are an error instead of a silent local backup.
|
||||||
|
- **SFTP variables are renamed, and the old names are no longer read:** `SSH_HOST`/`SSH_HOST_NAME` → `SFTP_HOST`, `SSH_PORT` → `SFTP_PORT`, `SSH_USER` → `SFTP_USER`, `SSH_PASSWORD` → `SFTP_PASSWORD`, `SSH_IDENTIFY_FILE`/`SSH_IDENTITY_FILE` → `SFTP_IDENTITY_FILE`, `SSH_REMOTE_PATH` → `REMOTE_PATH`.
|
||||||
|
- **SFTP host keys are verified by default.** Set `SFTP_KNOWN_HOSTS=/path/known_hosts`, or `SFTP_INSECURE_IGNORE_HOST_KEY=true` to keep the 1.x behaviour.
|
||||||
|
- **`MAIL_SKIP_TLS` is read with its documented meaning** (`true` skips certificate verification). 1.x had it inverted, so `MAIL_SKIP_TLS=false` (the value in `.env.example`) turned verification *off*. Use `MAIL_INSECURE_SKIP_VERIFY` from now on.
|
||||||
|
- **Retention only deletes indexed backups** (see the [configuration reference](../reference/)). 1.x deleted every file older than N days in the folder. Set `BACKUP_RETENTION_ADOPT_LEGACY=true` once to let retention handle 1.x backups, or `BACKUP_INDEX=false` for name-based retention.
|
||||||
|
- **Encryption fails closed**: a `GPG_PUBLIC_KEY` that cannot be read is an error (1.x uploaded plaintext). The env var now wins over the auto-detected `/config/gnupg` key.
|
||||||
|
- **Errors no longer kill the daemon**: a failed run is logged, notified and retried at the next schedule; other databases of the same run continue (`BACKUP_RESCUE_MODE` is no longer needed).
|
||||||
|
- **Restore** works from every storage (1.x had no SFTP restore), is case-insensitive about the storage name, and no longer goes through `sh -c`.
|
||||||
|
- **`migrate`** streams directly into the target (no intermediate file), creates the target database if needed, and `TARGET_DB_NAME` defaults to the source name; `-a` migrates every database.
|
||||||
|
- **Notification templates** are built in; custom ones still go in `/config/templates` (`TEMPLATES_DIR`).
|
||||||
|
- **Version** is injected with `-ldflags "-X main.Version=..."` (was `utils.Version`).
|
||||||
|
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
1. **SFTP users:** mount a `known_hosts` file and set `SFTP_KNOWN_HOSTS`, or set `SFTP_INSECURE_IGNORE_HOST_KEY=true`.
|
||||||
|
2. **Email notifications with `MAIL_SKIP_TLS=false`:** certificate verification is now on, as the name always promised. Remove the variable, or set `MAIL_INSECURE_SKIP_VERIFY=true` if your SMTP server has a self-signed certificate.
|
||||||
|
3. **Retention:** set `BACKUP_RETENTION_ADOPT_LEGACY=true` for one run if retention should also delete the backups made by 1.x.
|
||||||
|
4. **Quoted values in Compose files:** in the list syntax (`- AWS_DISABLE_SSL="false"`) the quotes are part of the value. v2 validates booleans and rejects `"false"`; write `- AWS_DISABLE_SSL=false`.
|
||||||
|
5. **`--user` runs:** `/backup` and `/tmp/backup` are writable by any user (sticky, like `/tmp`); mounted volumes need the usual permissions.
|
||||||
|
6. **Restoring 1.x backups:** nothing to do. `.sql`, `.sql.gz`, `.sql.gpg` and `.sql.gz.gpg` files from 1.x restore as before.
|
||||||
|
|
||||||
|
## New in v2
|
||||||
|
|
||||||
|
- MySQL servers are dumped with Oracle `mysqldump` 8.4, MariaDB servers with `mariadb-dump` 11.8 (flavour detected automatically).
|
||||||
|
- `DB_URL` (`mysql://user:password@host:3306/db?ssl-mode=required`), TLS modes, client certificates.
|
||||||
|
- age encryption, zstd compression, Google Cloud Storage, FTPS, extra storage copies.
|
||||||
|
- `verify`, `list`, `delete`, `prune`, `reconcile`, `config`, `health` commands; backup index with sha256 per backup.
|
||||||
|
- Slack, Discord, ntfy, webhook and healthcheck notifications; Prometheus metrics in daemon mode.
|
||||||
|
- `migrate -a`, and migrate without an intermediate file.
|
||||||
@@ -6,24 +6,22 @@ services:
|
|||||||
# for a list of available releases.
|
# for a list of available releases.
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup --storage s3 -d my-database
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --storage s3 -d my-database"
|
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
## AWS configurations
|
## AWS configurations
|
||||||
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
- AWS_S3_BUCKET_NAME=backup
|
- AWS_S3_BUCKET_NAME=backup
|
||||||
- AWS_REGION="us-west-2"
|
- AWS_REGION=us-west-2
|
||||||
- AWS_ACCESS_KEY=xxxx
|
- AWS_ACCESS_KEY=xxxx
|
||||||
- AWS_SECRET_KEY=xxxxx
|
- AWS_SECRET_KEY=xxxxx
|
||||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||||
- AWS_DISABLE_SSL="false"
|
- AWS_DISABLE_SSL=false
|
||||||
|
- AWS_FORCE_PATH_STYLE=true # true for S3 alternative such as Minio
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
|
# In production, it is advised to lock your image tag to a proper
|
||||||
|
# release version instead of using `latest`.
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup --dbname database_name
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --dbname database_name --mode scheduled --period "0 1 * * *"
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_USERNAME=userName
|
- DB_USER=userName
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
|
# See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||||
|
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
|
||||||
@@ -6,24 +6,24 @@ services:
|
|||||||
# for a list of available releases.
|
# for a list of available releases.
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup --storage s3 -d my-database
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --storage s3 -d my-database --mode scheduled --period "0 1 * * *"
|
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_NAME=database
|
- DB_NAME=database
|
||||||
- DB_USERNAME=username
|
- DB_USER=username
|
||||||
- DB_PASSWORD=password
|
- DB_PASSWORD=password
|
||||||
## AWS configurations
|
## AWS configurations
|
||||||
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
|
||||||
- AWS_S3_BUCKET_NAME=backup
|
- AWS_S3_BUCKET_NAME=backup
|
||||||
- AWS_REGION="us-west-2"
|
- AWS_REGION=us-west-2
|
||||||
- AWS_ACCESS_KEY=xxxx
|
- AWS_ACCESS_KEY=xxxx
|
||||||
- AWS_SECRET_KEY=xxxxx
|
- AWS_SECRET_KEY=xxxxx
|
||||||
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
## In case you are using S3 alternative such as Minio and your Minio instance is not secured, you change it to true
|
||||||
- AWS_DISABLE_SSL="false"
|
- AWS_DISABLE_SSL=false
|
||||||
|
- AWS_FORCE_PATH_STYLE=true # true for S3 alternative such as Minio
|
||||||
|
# See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
|
||||||
|
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
|
||||||
# mysql-bkup container must be connected to the same network with your database
|
# mysql-bkup container must be connected to the same network with your database
|
||||||
networks:
|
networks:
|
||||||
- web
|
- web
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
version: "3"
|
|
||||||
services:
|
services:
|
||||||
mysql-bkup:
|
mysql-bkup:
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
container_name: mysql-bkup
|
container_name: mysql-bkup
|
||||||
command:
|
command: backup --dbname database_name
|
||||||
- /bin/sh
|
|
||||||
- -c
|
|
||||||
- mysql-bkup backup --dbname database_name
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./backup:/backup
|
- ./backup:/backup
|
||||||
environment:
|
environment:
|
||||||
- DB_PORT=3306
|
- DB_PORT=3306
|
||||||
- DB_HOST=mysql
|
- DB_HOST=mysql
|
||||||
- DB_USERNAME=userName
|
- DB_USER=userName
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
- DB_PASSWORD=${DB_PASSWORD}
|
||||||
+18
-13
@@ -1,34 +1,37 @@
|
|||||||
piVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: CronJob
|
kind: Job
|
||||||
metadata:
|
metadata:
|
||||||
name: bkup-job
|
name: backup
|
||||||
spec:
|
spec:
|
||||||
schedule: "0 1 * * *"
|
|
||||||
jobTemplate:
|
|
||||||
spec:
|
|
||||||
template:
|
template:
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: mysql-bkup
|
- name: mysql-bkup
|
||||||
|
# In production, it is advised to lock your image tag to a proper
|
||||||
|
# release version instead of using `latest`.
|
||||||
|
# Check https://github.com/jkaninda/mysql-bkup/releases
|
||||||
|
# for a list of available releases.
|
||||||
image: jkaninda/mysql-bkup
|
image: jkaninda/mysql-bkup
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
- mysql-bkup backup -s s3 --path /custom_path
|
- backup --storage s3
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: "128Mi"
|
||||||
|
cpu: "500m"
|
||||||
env:
|
env:
|
||||||
- name: DB_PORT
|
- name: DB_PORT
|
||||||
value: "3306"
|
value: "3306"
|
||||||
- name: DB_HOST
|
- name: DB_HOST
|
||||||
value: ""
|
value: ""
|
||||||
- name: DB_NAME
|
- name: DB_NAME
|
||||||
value: ""
|
value: "dbname"
|
||||||
- name: DB_USERNAME
|
- name: DB_USER
|
||||||
value: ""
|
value: "username"
|
||||||
# Please use secret!
|
# Please use secret!
|
||||||
- name: DB_PASSWORD
|
- name: DB_PASSWORD
|
||||||
value: ""
|
value: ""
|
||||||
- name: ACCESS_KEY
|
|
||||||
value: ""
|
|
||||||
- name: AWS_S3_ENDPOINT
|
- name: AWS_S3_ENDPOINT
|
||||||
value: "https://s3.amazonaws.com"
|
value: "https://s3.amazonaws.com"
|
||||||
- name: AWS_S3_BUCKET_NAME
|
- name: AWS_S3_BUCKET_NAME
|
||||||
@@ -41,4 +44,6 @@ spec:
|
|||||||
value: "xxxx"
|
value: "xxxx"
|
||||||
- name: AWS_DISABLE_SSL
|
- name: AWS_DISABLE_SSL
|
||||||
value: "false"
|
value: "false"
|
||||||
restartPolicy: OnFailure
|
- name: AWS_FORCE_PATH_STYLE
|
||||||
|
value: "true"
|
||||||
|
restartPolicy: Never
|
||||||
@@ -1,21 +1,102 @@
|
|||||||
module github.com/jkaninda/mysql-bkup
|
module github.com/jkaninda/mysql-bkup
|
||||||
|
|
||||||
go 1.22.5
|
go 1.26.0
|
||||||
|
|
||||||
require github.com/spf13/pflag v1.0.5
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.55.3
|
filippo.io/age v1.3.2
|
||||||
github.com/bramvdbogaerde/go-scp v1.5.0
|
github.com/jkaninda/bkupkit v0.0.2
|
||||||
github.com/hpcloud/tail v1.0.0
|
|
||||||
github.com/spf13/cobra v1.8.0
|
|
||||||
golang.org/x/crypto v0.18.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
cel.dev/expr v0.25.2 // indirect
|
||||||
|
cloud.google.com/go v0.123.0 // indirect
|
||||||
|
cloud.google.com/go/auth v0.18.2 // indirect
|
||||||
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
|
cloud.google.com/go/iam v1.5.3 // indirect
|
||||||
|
cloud.google.com/go/monitoring v1.24.3 // indirect
|
||||||
|
cloud.google.com/go/storage v1.60.0 // indirect
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
filippo.io/hpke v0.4.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 // indirect
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
|
||||||
|
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.47.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.33.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.20.9 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.43 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 // indirect
|
||||||
|
github.com/aws/smithy-go v1.28.5 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
|
||||||
|
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.4 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect
|
||||||
|
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
|
||||||
|
github.com/goposta/posta-go v0.1.0 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jkaninda/encryptor v0.3.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
github.com/jkaninda/go-storage v0.2.0 // indirect
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
github.com/jkaninda/okapi v0.11.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
github.com/jlaffaye/ftp v0.2.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.20.3 // indirect
|
||||||
|
github.com/klauspost/pgzip v1.3.0 // indirect
|
||||||
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
|
github.com/pkg/sftp v1.13.10 // indirect
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/spf13/cobra v1.10.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/spiffe/go-spiffe/v2 v2.7.0 // indirect
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
|
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||||
|
golang.org/x/crypto v0.57.0 // indirect
|
||||||
|
golang.org/x/net v0.60.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.36.0 // indirect
|
||||||
|
golang.org/x/sync v0.23.0 // indirect
|
||||||
|
golang.org/x/sys v0.48.0 // indirect
|
||||||
|
golang.org/x/text v0.42.0 // indirect
|
||||||
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
google.golang.org/api v0.267.0 // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||||
|
google.golang.org/grpc v1.83.1 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
gopkg.in/mail.v2 v2.3.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,38 +1,247 @@
|
|||||||
github.com/aws/aws-sdk-go v1.55.3 h1:0B5hOX+mIx7I5XPOrjrHlKSDQV/+ypFZpIHOx5LOk3E=
|
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs=
|
||||||
github.com/aws/aws-sdk-go v1.55.3/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
|
||||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||||
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
|
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||||
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||||
|
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||||
|
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
|
||||||
|
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
|
||||||
|
cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY=
|
||||||
|
cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw=
|
||||||
|
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
|
||||||
|
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
|
||||||
|
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||||
|
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||||
|
cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8=
|
||||||
|
cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0=
|
||||||
|
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||||
|
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||||
|
filippo.io/age v1.3.2 h1:r6RSZLFSMm6rzKepZ7ZAYkKCu14f3/Me8c7uKYh7C8c=
|
||||||
|
filippo.io/age v1.3.2/go.mod h1:TH/Yr2sSRhCKbaH4XPxpUV0Us8Gv6txYUpiZQWz8Evk=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||||
|
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
|
||||||
|
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||||
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0 h1:l7+6kwRMJNwdCvYdDl7Eax+wzEYHSnNY7zrrfbhDdTA=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.33.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||||
|
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||||
|
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||||
|
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.47.3 h1:HRoT/MkNi3bjO5xF4dCzLzvVmkvRGHj3/cLINDnJejk=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.47.3/go.mod h1:xSl+OSI5ZUw6bSMupWc218zE5Pl+65MCCu3ySWFY99A=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22 h1:oiEmPz/YMMeh/JvAE8xSxNCIsvWE6lpFe4ZPlyFykQI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.22/go.mod h1:QWUDuRi9PFjBHIU2iH1Pkldbe7zqLfXJC4wWLbF8nuQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.33.4 h1:FzvkXKSzwqHni4U7nDigHg4jjtqMpVUuHgmZfSoJVQ0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.33.4/go.mod h1:VZqGZnZsCWVfK/iGPptJIyNIX3XEX6iQU2Rel4sLrr8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.20.9 h1:SNO55PnWhaDafsOcv16lCA0kdoFMl/YlegWoVsU02s0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.20.9/go.mod h1:+2reOTRtdD3VSBqpRDNwrZIYy8yMCYa7oK3WKTJ/I1I=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3 h1:gr3b87OLB29jymWPzp/CAyXwQRJVE+sC4Wm0qBIKHsk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.3/go.mod h1:BRjf4L/Yx2hY54o+ZibPbLJyMMeSwqYQXwcJfqW66a8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.43 h1:Fhx4uwBshQF0+jmhV/FKDM4LPj7L9iUy5n3MvLxLy00=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.43/go.mod h1:4Tyfc1eIPEyrAMysLCVkcYL1Hm/hkGWbElLo8pejHPo=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6 h1:CqL8NN3p4I0JTwwV5MEoatIl4Y2amZ2X9dRD+0hbRL0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.6/go.mod h1:exdJ32hMqXl0Wyo6NwqEC5yIhtua3+PDaKE8P29m4qE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6 h1:FpBq24TG6WVSObTNhQOv7Q0088rounUUlwn3zlNItkM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.6/go.mod h1:ONzdPVYC95oBOu0VWNEsKEntcoemP3xVKjT+qWml1s4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6 h1:Qmn60RWuk9+bd2ddvJJqwf3Cpac3VvGMOdbGemtUhX4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.6/go.mod h1:h/YJhe4AVyQxEUMluph10iqV7mjajH7A9L+BHF5ppis=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21 h1:e6YW5uME30TkrElR+4/xcY7NB6CCj2dcv7vtmGXhfhQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.21/go.mod h1:obpHAMP693+f3/A8PkLhIqfK1C+NWWn8zXzqD3OH520=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7 h1:CwvFlKw0uUScfgOMflXSRZa+ayXgOf6wk7lhyMemQYU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.7/go.mod h1:4SbD8BB+p+QEnG0aX6DoWwrtk9SufyYXRsUv0nve8LE=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6 h1:LHHdobjo5le1sjJdolitV7uVvYg3wkaeM1Ju2fDgeZQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.6/go.mod h1:CBGB0evAiaMD3Lk4wPO7LQCcCZLuhzL5Qf/wfEUHQ5w=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6 h1:mYViDr2azcgxOpRrObZOl631bvavqILoE/L+Kxgbwys=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.6/go.mod h1:OXVdAo9jNhggB+hJRGOyy7WolGTKB6Rg7IIulDeSzPM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3 h1:NyeuyXkZL476uoAowNActm4XJO1QPKf7aassgXHpiqA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.114.3/go.mod h1:GfoBeKMDJecgRUsGf/0RGdx0NJAh81ObTd922h8pQys=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4 h1:tvpDjuUvgKURf3Yk4hOaGbZ4QdY+070OWcA5d6LUv2A=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/signin v1.10.4/go.mod h1:3ibVKg5Ng82Zby8++jELTD2nHxiPk2DJRLEeW/LKp8M=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4 h1:ChCtagixSFOxltv0o6E3NT8F7ksvZtZGPwUBynO5Nmk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.38.4/go.mod h1:+FEO9jrVMpc9oN3CdAxJbtlQEu3z8UddDPSedpeM3PY=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4 h1:j6c+HA4AoS0EBEjArPjavFEBrucJEryfrQU0TffmACk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.4/go.mod h1:RhghaXUk9q66VtOQUX1FdPddQuHnqQZzbB0fEYXmPRA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4 h1:OKEfbf2tcfsF2pBk/S9QqNpo6alaAtiBs4hysCsBjPw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.51.4/go.mod h1:mCd6qGo9b8hTtfjgsmNkFlzwjyce4owd7HDVFnCLXhM=
|
||||||
|
github.com/aws/smithy-go v1.28.5 h1:OqbqKtzY9NUnr2yAe9+hiTdFb20tNaK/lI0PMOGXX9k=
|
||||||
|
github.com/aws/smithy-go v1.28.5/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||||
|
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||||
|
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||||
|
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||||
|
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||||
|
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||||
|
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||||
|
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||||
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||||
|
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
|
||||||
|
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
|
||||||
|
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
|
||||||
|
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao=
|
||||||
|
github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
|
||||||
|
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
|
||||||
|
github.com/goposta/posta-go v0.1.0 h1:UkMRZ2FAxUbjIZRiT9GIdrm4ypFtngKI/hZeC9hYB9Y=
|
||||||
|
github.com/goposta/posta-go v0.1.0/go.mod h1:IjYW+tr9L+3NAzPWScOKvxdi41x6JBL1ju63usJLgRM=
|
||||||
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||||
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
github.com/jkaninda/bkupkit v0.0.2 h1:3a5J/8qwXX7TWYBwIX57OQvUQCqo3er/JZZwvgU8I1g=
|
||||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
github.com/jkaninda/bkupkit v0.0.2/go.mod h1:TB2HVP8jRau/TdwfsO4QOgJY4Vi4h6HX+BlxJzXdcIg=
|
||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
github.com/jkaninda/encryptor v0.3.0 h1:m7r7ROyjXtVXpFTtdGAhHdcPLDD+6kb0eaTF9cY4KM4=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/jkaninda/encryptor v0.3.0/go.mod h1:ktF+sv4yLWa97xQt6Quhr1AWKwTnE7/HHEb6JJ3O7QU=
|
||||||
|
github.com/jkaninda/go-storage v0.2.0 h1:EjhIcu+zFkqTqnCleJqyS2TJfIC8U+V+Imw/qN3OVJM=
|
||||||
|
github.com/jkaninda/go-storage v0.2.0/go.mod h1:uJOFRtwwfq9yhj/yG3we5OTTg3JGeBv24/SrgmsSPKQ=
|
||||||
|
github.com/jkaninda/okapi v0.11.0 h1:73Ke2BJF5TTYG0DLKkFnSKrAP87bzErRQ6tOxF+DHF4=
|
||||||
|
github.com/jkaninda/okapi v0.11.0/go.mod h1:/Bt3U6MpUaGdb7iPKjyRpqFhv9/AIj/SReNbRjGQ1mw=
|
||||||
|
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
|
||||||
|
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
|
||||||
|
github.com/klauspost/compress v1.20.3 h1:zWNoFqq/lSuYQRim3dUXlXeA0IqwuZGicdG5ZTfY5j4=
|
||||||
|
github.com/klauspost/compress v1.20.3/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||||
|
github.com/klauspost/pgzip v1.3.0 h1:8t+KjjzD/0n/8vNlJGGZmTeK4njuwffOQvJm8tOSc8o=
|
||||||
|
github.com/klauspost/pgzip v1.3.0/go.mod h1:Xx7tAv5DH8ObLHUDQKXoE/nd8i/FYdNDTVMvi3SLTKI=
|
||||||
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
|
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||||
|
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||||
|
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||||
|
github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
|
||||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
|
||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
|
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||||
|
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8=
|
||||||
|
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI=
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||||
|
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||||
|
go.opentelemetry.io/otel/metric/x v0.67.0 h1:PcicCNZFkZ4bXfSooXdo3WN7RBOVOtjVdo1wD358Uns=
|
||||||
|
go.opentelemetry.io/otel/metric/x v0.67.0/go.mod h1:FBjCWZe6wgcqxcMtjdGiClDKXb2YxxXii0CXftE4QtI=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||||
|
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
||||||
|
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
||||||
|
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
||||||
|
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
||||||
|
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||||
|
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||||
|
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
||||||
|
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
|
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||||
|
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
|
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
||||||
|
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
||||||
|
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
||||||
|
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
||||||
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/api v0.267.0 h1:w+vfWPMPYeRs8qH1aYYsFX68jMls5acWl/jocfLomwE=
|
||||||
|
google.golang.org/api v0.267.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0=
|
||||||
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
|
||||||
|
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y=
|
||||||
|
google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||||
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
//go:build integration
|
||||||
|
|
||||||
|
// Integration tests: real MySQL / MariaDB servers in Docker, the real client
|
||||||
|
// tools, the whole bkupkit pipeline through cli.App.Run.
|
||||||
|
//
|
||||||
|
// The client tools run in containers too: shim scripts named mysql, mysqldump,
|
||||||
|
// mariadb and mariadb-dump are put first in PATH and `docker run -i` the
|
||||||
|
// official images (or IT_CLIENT_IMAGE, e.g. the mysql-bkup image itself). The
|
||||||
|
// private TMP_DIR holding the option files is bind-mounted at the same path.
|
||||||
|
//
|
||||||
|
// go test -tags integration -v -run . ./...
|
||||||
|
// IT_CLIENT_IMAGE=jkaninda/mysql-bkup:test go test -tags integration ./...
|
||||||
|
// IT_SKIP_MYSQL80=1 ... skip the MySQL 8.0 server
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"github.com/jkaninda/bkupkit/cli"
|
||||||
|
"github.com/jkaninda/mysql-bkup/internal/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rootPW = "r00t-pw"
|
||||||
|
// appPW exercises option-file quoting.
|
||||||
|
appPW = `p"a#s s\w'd$x`
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
itNetwork string
|
||||||
|
itTmp string // TMP_DIR shared with the client containers
|
||||||
|
itShims string
|
||||||
|
itSkip string
|
||||||
|
servers []*dbServer
|
||||||
|
minioPort string
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbServer struct {
|
||||||
|
name string // container name and DB_HOST
|
||||||
|
image string
|
||||||
|
client string // client binary inside the server image
|
||||||
|
mariadb bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miabi takes the LAST match of this regex in the output as the artifact.
|
||||||
|
var miabiDB = regexp.MustCompile(`[\w.\-]+\.(?:sql|archive)\.gz(?:\.gpg)?`)
|
||||||
|
|
||||||
|
func docker(args ...string) (string, error) {
|
||||||
|
// Stdout only on success: "docker run" prints image pull progress on stderr,
|
||||||
|
// which would otherwise end up in the container id on a runner without the image.
|
||||||
|
cmd := exec.Command("docker", args...)
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
out = append(out, stderr.Bytes()...)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if _, err := exec.LookPath("docker"); err != nil {
|
||||||
|
itSkip = "docker not installed"
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
if _, err := docker("info"); err != nil {
|
||||||
|
itSkip = "docker not running"
|
||||||
|
os.Exit(m.Run())
|
||||||
|
}
|
||||||
|
code := func() int {
|
||||||
|
suffix := fmt.Sprintf("%d", time.Now().UnixNano()%1e9)
|
||||||
|
itNetwork = "mysqlbkup-it-" + suffix
|
||||||
|
if out, err := docker("network", "create", itNetwork); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, out)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer docker("network", "rm", itNetwork)
|
||||||
|
var err error
|
||||||
|
if itTmp, err = os.MkdirTemp("", "mysqlbkup-it-tmp-"); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(itTmp)
|
||||||
|
// macOS: /var/folders is a symlink to /private/var/folders; mount the real path.
|
||||||
|
if p, err := filepath.EvalSymlinks(itTmp); err == nil {
|
||||||
|
itTmp = p
|
||||||
|
}
|
||||||
|
if itShims, err = os.MkdirTemp("", "mysqlbkup-it-bin-"); err != nil {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(itShims)
|
||||||
|
writeShims()
|
||||||
|
|
||||||
|
servers = []*dbServer{
|
||||||
|
{name: "it-mysql84-" + suffix, image: "mysql:8.4", client: "mysql"},
|
||||||
|
{name: "it-mariadb118-" + suffix, image: "mariadb:11.8", client: "mariadb", mariadb: true},
|
||||||
|
}
|
||||||
|
if os.Getenv("IT_SKIP_MYSQL80") == "" {
|
||||||
|
servers = append(servers, &dbServer{name: "it-mysql80-" + suffix, image: "mysql:8.0", client: "mysql"})
|
||||||
|
}
|
||||||
|
for _, s := range servers {
|
||||||
|
args := []string{"run", "-d", "--rm", "--name", s.name, "--network", itNetwork}
|
||||||
|
if s.mariadb {
|
||||||
|
args = append(args, "-e", "MARIADB_ROOT_PASSWORD="+rootPW)
|
||||||
|
} else {
|
||||||
|
args = append(args, "-e", "MYSQL_ROOT_PASSWORD="+rootPW)
|
||||||
|
}
|
||||||
|
if out, err := docker(append(args, s.image)...); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, out)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer docker("rm", "-f", s.name)
|
||||||
|
}
|
||||||
|
minio := "it-minio-" + suffix
|
||||||
|
if out, err := docker("run", "-d", "--rm", "--name", minio, "-p", "127.0.0.1::9000",
|
||||||
|
"-e", "MINIO_ROOT_USER=minioadmin", "-e", "MINIO_ROOT_PASSWORD=minioadmin",
|
||||||
|
"pgsty/minio:latest", "server", "/data"); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, out)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer docker("rm", "-f", minio)
|
||||||
|
for _, s := range servers {
|
||||||
|
if err := s.wait(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, err := docker("port", minio, "9000/tcp")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, out)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
minioPort = out[strings.LastIndex(out, ":")+1:]
|
||||||
|
deadline := time.Now().Add(60 * time.Second)
|
||||||
|
for {
|
||||||
|
out, err = docker("exec", minio, "sh", "-c", "mc alias set l http://127.0.0.1:9000 minioadmin minioadmin && mc mb --ignore-existing l/backups")
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
fmt.Fprintln(os.Stderr, "minio:", out)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
minioContainer = minio
|
||||||
|
return m.Run()
|
||||||
|
}()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var minioContainer string
|
||||||
|
|
||||||
|
func writeShims() {
|
||||||
|
imgs := map[string]string{"mysql": "mysql:8.4", "mysqldump": "mysql:8.4", "mariadb": "mariadb:11.8", "mariadb-dump": "mariadb:11.8"}
|
||||||
|
for bin, img := range imgs {
|
||||||
|
if ci := os.Getenv("IT_CLIENT_IMAGE"); ci != "" {
|
||||||
|
img = ci
|
||||||
|
}
|
||||||
|
sh := fmt.Sprintf("#!/bin/sh\nexec docker run --rm -i --network %s -v %s:%s --entrypoint %s %s \"$@\"\n", itNetwork, itTmp, itTmp, bin, img)
|
||||||
|
if err := os.WriteFile(filepath.Join(itShims, bin), []byte(sh), 0o755); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sql runs statements as root inside the server container.
|
||||||
|
func (s *dbServer) sql(stmts string) (string, error) {
|
||||||
|
cmd := exec.Command("docker", "exec", "-i", "-e", "MYSQL_PWD="+rootPW, s.name, s.client, "-uroot", "-h127.0.0.1", "-N", "-B")
|
||||||
|
cmd.Stdin = strings.NewReader(stmts)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
return strings.TrimSpace(string(out)), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbServer) wait() error {
|
||||||
|
deadline := time.Now().Add(3 * time.Minute)
|
||||||
|
for {
|
||||||
|
// TCP only answers once the image's init server is gone.
|
||||||
|
if _, err := s.sql("SELECT 1"); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
logs, _ := docker("logs", s.name)
|
||||||
|
return fmt.Errorf("%s not ready:\n%s", s.name, logs)
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *dbServer) mustSQL(t *testing.T, stmts string) string {
|
||||||
|
t.Helper()
|
||||||
|
out, err := s.sql(stmts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v\n%s", s.name, err, out)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = `
|
||||||
|
DROP DATABASE IF EXISTS %[1]s;
|
||||||
|
CREATE DATABASE %[1]s CHARACTER SET utf8mb4;
|
||||||
|
USE %[1]s;
|
||||||
|
CREATE TABLE products (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100), price DECIMAL(10,2), data BLOB, created DATETIME);
|
||||||
|
CREATE TABLE audit (id INT PRIMARY KEY AUTO_INCREMENT, product_id INT, note TEXT);
|
||||||
|
CREATE TRIGGER trg_products AFTER INSERT ON products FOR EACH ROW INSERT INTO audit (product_id, note) VALUES (NEW.id, 'created');
|
||||||
|
CREATE PROCEDURE p_count() SELECT COUNT(*) FROM products;
|
||||||
|
CREATE VIEW v_products AS SELECT id, name FROM products;
|
||||||
|
CREATE EVENT ev_cleanup ON SCHEDULE EVERY 1 DAY DO DELETE FROM audit WHERE id < 0;
|
||||||
|
INSERT INTO products (name, price, data, created) VALUES
|
||||||
|
('café 😀', 9.99, X'00FF0A0D27225C', '2026-01-02 03:04:05'),
|
||||||
|
('it''s "quoted" \\ back', 0.01, NULL, NULL),
|
||||||
|
('tab\tnew\nline', 12345678.90, REPEAT(X'AB', 60000), '1999-12-31 23:59:59');
|
||||||
|
INSERT INTO products (name, price) SELECT CONCAT('bulk-', a.n * 10 + b.n), a.n + b.n FROM
|
||||||
|
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a,
|
||||||
|
(SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b;
|
||||||
|
`
|
||||||
|
|
||||||
|
func (s *dbServer) loadFixture(t *testing.T, db string) {
|
||||||
|
t.Helper()
|
||||||
|
s.mustSQL(t, fmt.Sprintf(fixture, db))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fingerprint summarises the data and objects of db.
|
||||||
|
func (s *dbServer) fingerprint(t *testing.T, db string) string {
|
||||||
|
t.Helper()
|
||||||
|
q := fmt.Sprintf(`SET SESSION group_concat_max_len = 100000000;
|
||||||
|
SELECT COUNT(*), MD5(GROUP_CONCAT(CONCAT_WS('|', id, name, price, HEX(data), created) ORDER BY id SEPARATOR ';')) FROM %[1]s.products;
|
||||||
|
SELECT COUNT(*) FROM %[1]s.audit;
|
||||||
|
SELECT COUNT(*) FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = '%[1]s';
|
||||||
|
SELECT COUNT(*) FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = '%[1]s';
|
||||||
|
SELECT COUNT(*) FROM information_schema.VIEWS WHERE TABLE_SCHEMA = '%[1]s';
|
||||||
|
SELECT COUNT(*) FROM information_schema.EVENTS WHERE EVENT_SCHEMA = '%[1]s';`, db)
|
||||||
|
return s.mustSQL(t, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
func needDocker(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if itSkip != "" {
|
||||||
|
t.Skip(itSkip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run executes the CLI in-process and returns its combined output.
|
||||||
|
func run(t *testing.T, args ...string) (string, int) {
|
||||||
|
t.Helper()
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
oldOut, oldErr := os.Stdout, os.Stderr
|
||||||
|
os.Stdout, os.Stderr = w, w
|
||||||
|
var buf bytes.Buffer
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() { _, _ = io.Copy(&buf, r); close(done) }()
|
||||||
|
code := (&cli.App{Engine: mysql.New(), Version: "it"}).Run(args)
|
||||||
|
_ = w.Close()
|
||||||
|
<-done
|
||||||
|
os.Stdout, os.Stderr = oldOut, oldErr
|
||||||
|
return buf.String(), code
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRun(t *testing.T, args ...string) string {
|
||||||
|
t.Helper()
|
||||||
|
out, code := run(t, args...)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("%v: exit %d\n%s", args, code, out)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// setEnv sets the common environment for s and returns the local storage dir.
|
||||||
|
func setEnv(t *testing.T, s *dbServer, user, pw string) string {
|
||||||
|
t.Helper()
|
||||||
|
store := t.TempDir()
|
||||||
|
t.Setenv("PATH", itShims+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
t.Setenv("TMP_DIR", itTmp)
|
||||||
|
t.Setenv("STORAGE_PATH", store)
|
||||||
|
t.Setenv("HEALTH_FILE", filepath.Join(t.TempDir(), "health.json"))
|
||||||
|
t.Setenv("TEMPLATES_DIR", "")
|
||||||
|
t.Setenv("DB_HOST", s.name)
|
||||||
|
t.Setenv("DB_PORT", "3306")
|
||||||
|
t.Setenv("DB_USERNAME", user)
|
||||||
|
t.Setenv("DB_PASSWORD", pw)
|
||||||
|
return store
|
||||||
|
}
|
||||||
|
|
||||||
|
// artifact returns the last Miabi regex match and checks the file exists in dir.
|
||||||
|
func artifact(t *testing.T, out, dir string) string {
|
||||||
|
t.Helper()
|
||||||
|
m := miabiDB.FindAllString(out, -1)
|
||||||
|
if len(m) == 0 {
|
||||||
|
t.Fatalf("no artifact name in output:\n%s", out)
|
||||||
|
}
|
||||||
|
last := m[len(m)-1]
|
||||||
|
if dir != "" {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, last)); err != nil {
|
||||||
|
t.Fatalf("last match %q is not the stored artifact: %v\n%s", last, err, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertTmpClean(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
if ents, _ := os.ReadDir(itTmp); len(ents) != 0 {
|
||||||
|
var names []string
|
||||||
|
for _, e := range ents {
|
||||||
|
names = append(names, e.Name())
|
||||||
|
}
|
||||||
|
t.Fatalf("TMP_DIR not empty: %v", names)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forEachServer(t *testing.T, fn func(t *testing.T, s *dbServer)) {
|
||||||
|
needDocker(t)
|
||||||
|
for _, s := range servers {
|
||||||
|
t.Run(strings.SplitN(strings.TrimPrefix(s.name, "it-"), "-", 2)[0], func(t *testing.T) { fn(t, s) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSingleDatabase: Miabi's argv (backup -d db, restore -d db -f file),
|
||||||
|
// gzip default, restore after data loss, the client chosen per flavour.
|
||||||
|
func TestSingleDatabase(t *testing.T) {
|
||||||
|
forEachServer(t, func(t *testing.T, s *dbServer) {
|
||||||
|
s.loadFixture(t, "shop")
|
||||||
|
want := s.fingerprint(t, "shop")
|
||||||
|
store := setEnv(t, s, "root", rootPW)
|
||||||
|
|
||||||
|
out := mustRun(t, "backup", "-d", "shop")
|
||||||
|
file := artifact(t, out, store)
|
||||||
|
if !strings.HasSuffix(file, ".sql.gz") || !strings.HasPrefix(file, "shop_") {
|
||||||
|
t.Fatalf("artifact %q", file)
|
||||||
|
}
|
||||||
|
wantClient := "client=mysqldump"
|
||||||
|
if s.mariadb {
|
||||||
|
wantClient = "client=mariadb-dump"
|
||||||
|
}
|
||||||
|
if !strings.Contains(out, wantClient) {
|
||||||
|
t.Fatalf("expected %s:\n%s", wantClient, out)
|
||||||
|
}
|
||||||
|
assertTmpClean(t)
|
||||||
|
|
||||||
|
s.mustSQL(t, "DROP DATABASE shop")
|
||||||
|
mustRun(t, "restore", "-d", "shop", "-f", file)
|
||||||
|
if got := s.fingerprint(t, "shop"); got != want {
|
||||||
|
t.Fatalf("restored data differs:\n got %s\nwant %s", got, want)
|
||||||
|
}
|
||||||
|
assertTmpClean(t)
|
||||||
|
|
||||||
|
// verify and list work without a database connection.
|
||||||
|
mustRun(t, "verify", "-f", file)
|
||||||
|
if out := mustRun(t, "list"); !strings.Contains(out, file) {
|
||||||
|
t.Fatalf("list:\n%s", out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNonRootUser: a user with only database privileges (no PROCESS, no
|
||||||
|
// global SELECT) and a password that needs option-file quoting.
|
||||||
|
func TestNonRootUser(t *testing.T) {
|
||||||
|
forEachServer(t, func(t *testing.T, s *dbServer) {
|
||||||
|
s.mustSQL(t, fmt.Sprintf(`DROP USER IF EXISTS 'app'@'%%'; CREATE USER 'app'@'%%' IDENTIFIED BY '%s';
|
||||||
|
DROP DATABASE IF EXISTS appdb; CREATE DATABASE appdb; GRANT ALL ON appdb.* TO 'app'@'%%';
|
||||||
|
CREATE TABLE appdb.items (id INT PRIMARY KEY, v VARCHAR(20)); INSERT INTO appdb.items VALUES (1,'a'),(2,'b');`,
|
||||||
|
strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(appPW)))
|
||||||
|
store := setEnv(t, s, "app", appPW)
|
||||||
|
out := mustRun(t, "backup", "-d", "appdb")
|
||||||
|
file := artifact(t, out, store)
|
||||||
|
s.mustSQL(t, "DELETE FROM appdb.items")
|
||||||
|
mustRun(t, "restore", "-d", "appdb", "-f", file)
|
||||||
|
if got := s.mustSQL(t, "SELECT COUNT(*) FROM appdb.items"); got != "2" {
|
||||||
|
t.Fatalf("rows after restore: %s", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncryption: GPG passphrase and age recipients, restored under another name.
|
||||||
|
func TestEncryption(t *testing.T) {
|
||||||
|
forEachServer(t, func(t *testing.T, s *dbServer) {
|
||||||
|
s.loadFixture(t, "shop")
|
||||||
|
want := s.fingerprint(t, "shop")
|
||||||
|
store := setEnv(t, s, "root", rootPW)
|
||||||
|
|
||||||
|
t.Run("gpg", func(t *testing.T) {
|
||||||
|
t.Setenv("GPG_PASSPHRASE", "correct horse")
|
||||||
|
out := mustRun(t, "backup", "-d", "shop")
|
||||||
|
file := artifact(t, out, store)
|
||||||
|
if !strings.HasSuffix(file, ".sql.gz.gpg") {
|
||||||
|
t.Fatalf("artifact %q", file)
|
||||||
|
}
|
||||||
|
s.mustSQL(t, "DROP DATABASE IF EXISTS shop_gpg")
|
||||||
|
mustRun(t, "restore", "-d", "shop_gpg", "-f", file)
|
||||||
|
if got := s.fingerprint(t, "shop_gpg"); got != want {
|
||||||
|
t.Fatalf("restored data differs:\n got %s\nwant %s", got, want)
|
||||||
|
}
|
||||||
|
t.Setenv("GPG_PASSPHRASE", "wrong")
|
||||||
|
if out, code := run(t, "restore", "-d", "shop_gpg", "-f", file); code == 0 {
|
||||||
|
t.Fatalf("restore with a wrong passphrase succeeded:\n%s", out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("age", func(t *testing.T) {
|
||||||
|
id, err := age.GenerateX25519Identity()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("AGE_RECIPIENTS", id.Recipient().String())
|
||||||
|
out := mustRun(t, "backup", "-d", "shop")
|
||||||
|
m := regexp.MustCompile(`shop_[\w.\-]+\.sql\.gz\.age`).FindAllString(out, -1)
|
||||||
|
if len(m) == 0 {
|
||||||
|
t.Fatalf("no age artifact:\n%s", out)
|
||||||
|
}
|
||||||
|
file := m[len(m)-1]
|
||||||
|
s.mustSQL(t, "DROP DATABASE IF EXISTS shop_age")
|
||||||
|
t.Setenv("AGE_IDENTITY", id.String())
|
||||||
|
mustRun(t, "restore", "-d", "shop_age", "-f", file)
|
||||||
|
if got := s.fingerprint(t, "shop_age"); got != want {
|
||||||
|
t.Fatalf("restored data differs:\n got %s\nwant %s", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assertTmpClean(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAllDatabases: one file per database (-a, --exclude-db) and one file for
|
||||||
|
// everything (-A), restored without -d.
|
||||||
|
func TestAllDatabases(t *testing.T) {
|
||||||
|
forEachServer(t, func(t *testing.T, s *dbServer) {
|
||||||
|
s.mustSQL(t, "DROP DATABASE IF EXISTS shop_gpg; DROP DATABASE IF EXISTS shop_age; DROP DATABASE IF EXISTS appdb; DROP DATABASE IF EXISTS skipme; CREATE DATABASE skipme;")
|
||||||
|
s.loadFixture(t, "shop")
|
||||||
|
s.loadFixture(t, "blog")
|
||||||
|
wantShop, wantBlog := s.fingerprint(t, "shop"), s.fingerprint(t, "blog")
|
||||||
|
store := setEnv(t, s, "root", rootPW)
|
||||||
|
|
||||||
|
out := mustRun(t, "backup", "-a", "--exclude-db", "skipme")
|
||||||
|
ents, _ := os.ReadDir(store)
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, e := range ents {
|
||||||
|
if strings.HasSuffix(e.Name(), ".sql.gz") {
|
||||||
|
seen[e.Name()[:strings.Index(e.Name(), "_2")]] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !seen["shop"] || !seen["blog"] || seen["skipme"] || seen["mysql"] || seen["sys"] {
|
||||||
|
t.Fatalf("artifacts %v\n%s", seen, out)
|
||||||
|
}
|
||||||
|
artifact(t, out, store)
|
||||||
|
|
||||||
|
out = mustRun(t, "backup", "-A")
|
||||||
|
file := artifact(t, out, store)
|
||||||
|
if !strings.HasPrefix(file, mysql.AllInOneName+"_") {
|
||||||
|
t.Fatalf("all-in-one artifact %q", file)
|
||||||
|
}
|
||||||
|
s.mustSQL(t, "DROP DATABASE shop; DROP DATABASE blog")
|
||||||
|
t.Setenv("DB_NAME", "")
|
||||||
|
mustRun(t, "restore", "-f", file)
|
||||||
|
if got := s.fingerprint(t, "shop"); got != wantShop {
|
||||||
|
t.Fatalf("shop differs:\n got %s\nwant %s", got, wantShop)
|
||||||
|
}
|
||||||
|
if got := s.fingerprint(t, "blog"); got != wantBlog {
|
||||||
|
t.Fatalf("blog differs:\n got %s\nwant %s", got, wantBlog)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUploadsAndCorruption: Miabi uploads of plain .sql and gzip .sql.gz
|
||||||
|
// restore; corrupted inputs fail with a non-zero exit, and damaged or tampered
|
||||||
|
// archives are caught before the client runs, so the database is untouched.
|
||||||
|
func TestUploadsAndCorruption(t *testing.T) {
|
||||||
|
forEachServer(t, func(t *testing.T, s *dbServer) {
|
||||||
|
s.loadFixture(t, "shop")
|
||||||
|
want := s.fingerprint(t, "shop")
|
||||||
|
store := setEnv(t, s, "root", rootPW)
|
||||||
|
out := mustRun(t, "backup", "-d", "shop", "--disable-compression")
|
||||||
|
m := regexp.MustCompile(`shop_\d{8}_\d{6}(?:_\d+)?\.sql\b`).FindAllString(out, -1)
|
||||||
|
if len(m) == 0 {
|
||||||
|
t.Fatalf("no .sql artifact:\n%s", out)
|
||||||
|
}
|
||||||
|
plain, err := os.ReadFile(filepath.Join(store, m[len(m)-1]))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
write := func(name string, b []byte) {
|
||||||
|
if err := os.WriteFile(filepath.Join(store, name), b, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write("upload.sql", plain)
|
||||||
|
var gz bytes.Buffer
|
||||||
|
zw := gzip.NewWriter(&gz)
|
||||||
|
_, _ = zw.Write(plain)
|
||||||
|
_ = zw.Close()
|
||||||
|
write("upload.sql.gz", gz.Bytes())
|
||||||
|
write("truncated.sql.gz", gz.Bytes()[:gz.Len()/2])
|
||||||
|
write("bad.sql", append(append([]byte{}, plain...), []byte("\nTHIS IS NOT SQL;\n")...))
|
||||||
|
|
||||||
|
// 1.x style absolute path: restore -f /backup/upload.sql.gz
|
||||||
|
for _, f := range []string{"upload.sql", "upload.sql.gz", filepath.Join(store, "upload.sql.gz")} {
|
||||||
|
s.mustSQL(t, "DROP DATABASE IF EXISTS shop_up")
|
||||||
|
mustRun(t, "restore", "-d", "shop_up", "-f", f)
|
||||||
|
if got := s.fingerprint(t, "shop_up"); got != want {
|
||||||
|
t.Fatalf("%s: restored data differs", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A valid file with a bad statement cannot be caught before it runs.
|
||||||
|
for _, f := range []string{"bad.sql", "missing.sql.gz"} {
|
||||||
|
if out, code := run(t, "restore", "-d", "shop_bad", "-f", f); code == 0 {
|
||||||
|
t.Fatalf("%s: restore succeeded:\n%s", f, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A damaged archive is verified first and never reaches the database.
|
||||||
|
if out, code := run(t, "restore", "-d", "shop", "-f", "truncated.sql.gz"); code == 0 {
|
||||||
|
t.Fatalf("truncated.sql.gz: restore succeeded:\n%s", out)
|
||||||
|
}
|
||||||
|
if got := s.fingerprint(t, "shop"); got != want {
|
||||||
|
t.Fatal("truncated.sql.gz changed the database")
|
||||||
|
}
|
||||||
|
// Tampered GPG artifact.
|
||||||
|
t.Setenv("GPG_PASSPHRASE", "pw")
|
||||||
|
out = mustRun(t, "backup", "-d", "shop")
|
||||||
|
file := artifact(t, out, store)
|
||||||
|
b, _ := os.ReadFile(filepath.Join(store, file))
|
||||||
|
b[len(b)/2] ^= 0xFF
|
||||||
|
write("tampered.sql.gz.gpg", b)
|
||||||
|
if out, code := run(t, "restore", "-d", "shop", "-f", "tampered.sql.gz.gpg"); code == 0 {
|
||||||
|
t.Fatalf("tampered restore succeeded:\n%s", out)
|
||||||
|
}
|
||||||
|
if got := s.fingerprint(t, "shop"); got != want {
|
||||||
|
t.Fatal("tampered.sql.gz.gpg changed the database")
|
||||||
|
}
|
||||||
|
assertTmpClean(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMiabiS3: Miabi's exact S3 argv and env against MinIO.
|
||||||
|
func TestMiabiS3(t *testing.T) {
|
||||||
|
forEachServer(t, func(t *testing.T, s *dbServer) {
|
||||||
|
s.loadFixture(t, "shop")
|
||||||
|
want := s.fingerprint(t, "shop")
|
||||||
|
setEnv(t, s, "root", rootPW)
|
||||||
|
t.Setenv("AWS_S3_ENDPOINT", "http://127.0.0.1:"+minioPort)
|
||||||
|
t.Setenv("AWS_S3_BUCKET_NAME", "backups")
|
||||||
|
t.Setenv("AWS_REGION", "us-east-1")
|
||||||
|
t.Setenv("AWS_ACCESS_KEY", "minioadmin")
|
||||||
|
t.Setenv("AWS_SECRET_KEY", "minioadmin")
|
||||||
|
t.Setenv("AWS_DISABLE_SSL", "true")
|
||||||
|
t.Setenv("AWS_FORCE_PATH_STYLE", "true")
|
||||||
|
t.Setenv("GPG_PASSPHRASE", "miabi")
|
||||||
|
path := "miabi/" + strings.SplitN(s.name, "-", 3)[1]
|
||||||
|
|
||||||
|
var out string
|
||||||
|
for i := 0; i < 2; i++ { // two runs: older names are printed too
|
||||||
|
out = mustRun(t, "backup", "--storage", "s3", "-d", "shop", "--path", path)
|
||||||
|
}
|
||||||
|
file := artifact(t, out, "")
|
||||||
|
if !strings.HasSuffix(file, ".sql.gz.gpg") {
|
||||||
|
t.Fatalf("artifact %q", file)
|
||||||
|
}
|
||||||
|
if o, err := docker("exec", minioContainer, "mc", "stat", "l/backups/"+path+"/"+file); err != nil {
|
||||||
|
t.Fatalf("object %s/%s not in the bucket: %s", path, file, o)
|
||||||
|
}
|
||||||
|
s.mustSQL(t, "DROP DATABASE shop")
|
||||||
|
mustRun(t, "restore", "--storage", "s3", "-d", "shop", "--path", path, "-f", file)
|
||||||
|
if got := s.fingerprint(t, "shop"); got != want {
|
||||||
|
t.Fatalf("restored data differs")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMigrate: dump | restore between two servers, single database and -a.
|
||||||
|
func TestMigrate(t *testing.T) {
|
||||||
|
needDocker(t)
|
||||||
|
src := servers[0] // MySQL 8.4
|
||||||
|
dst := servers[len(servers)-1]
|
||||||
|
if dst.mariadb || dst == src {
|
||||||
|
t.Skip("needs a second MySQL server (IT_SKIP_MYSQL80 is set)")
|
||||||
|
}
|
||||||
|
src.loadFixture(t, "shop")
|
||||||
|
src.loadFixture(t, "blog")
|
||||||
|
want := src.fingerprint(t, "shop")
|
||||||
|
setEnv(t, src, "root", rootPW)
|
||||||
|
t.Setenv("TARGET_DB_HOST", dst.name)
|
||||||
|
t.Setenv("TARGET_DB_USERNAME", "root")
|
||||||
|
t.Setenv("TARGET_DB_PASSWORD", rootPW)
|
||||||
|
|
||||||
|
dst.mustSQL(t, "DROP DATABASE IF EXISTS shop_copy")
|
||||||
|
t.Setenv("TARGET_DB_NAME", "shop_copy")
|
||||||
|
mustRun(t, "migrate", "-d", "shop")
|
||||||
|
if got := dst.fingerprint(t, "shop_copy"); got != want {
|
||||||
|
t.Fatalf("migrated data differs:\n got %s\nwant %s", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("TARGET_DB_NAME", "")
|
||||||
|
dst.mustSQL(t, "DROP DATABASE IF EXISTS shop; DROP DATABASE IF EXISTS blog")
|
||||||
|
src.mustSQL(t, "DROP DATABASE IF EXISTS shop_gpg; DROP DATABASE IF EXISTS shop_age; DROP DATABASE IF EXISTS shop_up; DROP DATABASE IF EXISTS appdb; DROP DATABASE IF EXISTS skipme; DROP DATABASE IF EXISTS shop_bad")
|
||||||
|
mustRun(t, "migrate", "-a")
|
||||||
|
if got := dst.fingerprint(t, "blog"); got != src.fingerprint(t, "blog") {
|
||||||
|
t.Fatalf("blog differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// A failing target aborts the migration with a non-zero exit.
|
||||||
|
t.Setenv("TARGET_DB_PASSWORD", "wrong")
|
||||||
|
if out, code := run(t, "migrate", "-d", "shop"); code == 0 {
|
||||||
|
t.Fatalf("migrate with bad target credentials succeeded:\n%s", out)
|
||||||
|
}
|
||||||
|
assertTmpClean(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMigrateMySQLToMariaDB: cross-flavour migrate (Oracle mysqldump | mariadb).
|
||||||
|
func TestMigrateMySQLToMariaDB(t *testing.T) {
|
||||||
|
needDocker(t)
|
||||||
|
var src, dst *dbServer
|
||||||
|
for _, s := range servers {
|
||||||
|
if s.mariadb {
|
||||||
|
dst = s
|
||||||
|
} else if src == nil {
|
||||||
|
src = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
src.mustSQL(t, "DROP DATABASE IF EXISTS plain; CREATE DATABASE plain; CREATE TABLE plain.t (id INT PRIMARY KEY, v VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci); INSERT INTO plain.t VALUES (1,'x'),(2,'y');")
|
||||||
|
setEnv(t, src, "root", rootPW)
|
||||||
|
t.Setenv("TARGET_DB_HOST", dst.name)
|
||||||
|
t.Setenv("TARGET_DB_USERNAME", "root")
|
||||||
|
t.Setenv("TARGET_DB_PASSWORD", rootPW)
|
||||||
|
t.Setenv("TARGET_DB_NAME", "plain")
|
||||||
|
dst.mustSQL(t, "DROP DATABASE IF EXISTS plain")
|
||||||
|
mustRun(t, "migrate", "-d", "plain")
|
||||||
|
if got := dst.mustSQL(t, "SELECT COUNT(*) FROM plain.t"); got != "2" {
|
||||||
|
t.Fatalf("rows: %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jkaninda/bkupkit/config"
|
||||||
|
"github.com/jkaninda/bkupkit/execx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client binaries. Debian links mysql/mysqldump to the MariaDB tools, so the
|
||||||
|
// Oracle ones are recognized by their --version output, not by name.
|
||||||
|
const (
|
||||||
|
mariadbCLI = "mariadb"
|
||||||
|
mariadbDump = "mariadb-dump"
|
||||||
|
mysqlCLI = "mysql"
|
||||||
|
mysqlDump = "mysqldump"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client preferences (MYSQL_CLIENT).
|
||||||
|
const (
|
||||||
|
ClientAuto = "auto"
|
||||||
|
ClientMySQL = "mysql"
|
||||||
|
ClientMariaDB = "mariadb"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tool is a resolved client binary.
|
||||||
|
type tool struct {
|
||||||
|
name string // logical name (mysqldump, mariadb-dump, ...)
|
||||||
|
path string
|
||||||
|
mariadb bool // MariaDB client (option names differ)
|
||||||
|
}
|
||||||
|
|
||||||
|
// server is what Ping learned about a server.
|
||||||
|
type server struct {
|
||||||
|
version string
|
||||||
|
mariadb bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMariaDB reports whether a SELECT VERSION() result is a MariaDB server.
|
||||||
|
func IsMariaDB(version string) bool {
|
||||||
|
return strings.Contains(strings.ToLower(version), "mariadb")
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookTool resolves a binary and its client family (cached).
|
||||||
|
func (e *Engine) lookTool(ctx context.Context, name string) (tool, bool) {
|
||||||
|
e.mu.Lock()
|
||||||
|
t, ok := e.tools[name]
|
||||||
|
e.mu.Unlock()
|
||||||
|
if ok {
|
||||||
|
return t, t.path != ""
|
||||||
|
}
|
||||||
|
t = tool{name: name}
|
||||||
|
if p, err := e.lookPath(name); err == nil {
|
||||||
|
t.path = p
|
||||||
|
switch name {
|
||||||
|
case mariadbCLI, mariadbDump:
|
||||||
|
t.mariadb = true
|
||||||
|
default:
|
||||||
|
out, err := execx.Output(ctx, p, []string{"--version"}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.path = ""
|
||||||
|
} else {
|
||||||
|
t.mariadb = IsMariaDB(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.mu.Lock()
|
||||||
|
e.tools[name] = t
|
||||||
|
e.mu.Unlock()
|
||||||
|
return t, t.path != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) lookPath(name string) (string, error) {
|
||||||
|
if e.LookPath != nil {
|
||||||
|
return e.LookPath(name)
|
||||||
|
}
|
||||||
|
return exec.LookPath(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// oracle returns the Oracle MySQL binary called name, if installed.
|
||||||
|
func (e *Engine) oracle(ctx context.Context, name string) (tool, bool) {
|
||||||
|
t, ok := e.lookTool(ctx, name)
|
||||||
|
return t, ok && !t.mariadb
|
||||||
|
}
|
||||||
|
|
||||||
|
// maria returns the MariaDB binary (mariadb-dump, or a mysqldump that is MariaDB's).
|
||||||
|
func (e *Engine) maria(ctx context.Context, name, legacy string) (tool, bool) {
|
||||||
|
if t, ok := e.lookTool(ctx, name); ok {
|
||||||
|
return t, true
|
||||||
|
}
|
||||||
|
t, ok := e.lookTool(ctx, legacy)
|
||||||
|
return t, ok && t.mariadb
|
||||||
|
}
|
||||||
|
|
||||||
|
// pick chooses the dump (dump=true) or command-line client for a server:
|
||||||
|
// mariadb-dump/mariadb for MariaDB, Oracle mysqldump/mysql for MySQL.
|
||||||
|
func (e *Engine) pick(ctx context.Context, pref string, srv server, dump bool) (tool, error) {
|
||||||
|
oName, mName := mysqlCLI, mariadbCLI
|
||||||
|
if dump {
|
||||||
|
oName, mName = mysqlDump, mariadbDump
|
||||||
|
}
|
||||||
|
switch pref {
|
||||||
|
case ClientMySQL:
|
||||||
|
if t, ok := e.oracle(ctx, oName); ok {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
return tool{}, fmt.Errorf("MYSQL_CLIENT=mysql but the Oracle MySQL %s client is not installed", oName)
|
||||||
|
case ClientMariaDB:
|
||||||
|
if t, ok := e.maria(ctx, mName, oName); ok {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
return tool{}, fmt.Errorf("MYSQL_CLIENT=mariadb but %s is not installed", mName)
|
||||||
|
}
|
||||||
|
if srv.mariadb {
|
||||||
|
if t, ok := e.maria(ctx, mName, oName); ok {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
if t, ok := e.oracle(ctx, oName); ok {
|
||||||
|
e.warnOnce("oracle-for-mariadb", "MariaDB client not installed; using the Oracle MySQL client against a MariaDB server", "client", t.name)
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
return tool{}, fmt.Errorf("no %s or %s client installed", mName, oName)
|
||||||
|
}
|
||||||
|
if t, ok := e.oracle(ctx, oName); ok {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
if t, ok := e.maria(ctx, mName, oName); ok {
|
||||||
|
e.warnOnce("mariadb-for-mysql",
|
||||||
|
"Oracle MySQL client not available on this platform; using the MariaDB client against a MySQL server "+
|
||||||
|
"(dumps of MySQL-only features may be incomplete)",
|
||||||
|
"client", t.name, "server_version", srv.version)
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
return tool{}, fmt.Errorf("no %s or %s client installed", oName, mName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) warnOnce(key, msg string, args ...any) {
|
||||||
|
e.mu.Lock()
|
||||||
|
seen := e.warned[key]
|
||||||
|
e.warned[key] = true
|
||||||
|
e.mu.Unlock()
|
||||||
|
if !seen {
|
||||||
|
slog.Default().Warn(msg, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// withOptionFile writes c's option file (0600, private directory) and calls fn
|
||||||
|
// with its path. The file is removed when fn returns.
|
||||||
|
func withOptionFile(v *config.Values, c Conn, t tool, fn func(path string) error) error {
|
||||||
|
dir, cleanup, err := execx.PrivateDir(v.String(config.TmpDir))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create private directory: %w", err)
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
p, err := execx.WriteSecret(dir, "my.cnf", optionFile(c, t.mariadb))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write client option file: %w", err)
|
||||||
|
}
|
||||||
|
return fn(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// query runs one SQL statement with the command-line client and returns the
|
||||||
|
// raw tab-separated output without headers.
|
||||||
|
func query(ctx context.Context, v *config.Values, c Conn, t tool, sql string) (string, error) {
|
||||||
|
var out string
|
||||||
|
err := withOptionFile(v, c, t, func(cnf string) error {
|
||||||
|
var err error
|
||||||
|
out, err = execx.Output(ctx, t.path, []string{"--defaults-extra-file=" + cnf, "-N", "-B", "-r", "-e", sql}, nil)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// server detects the server version and flavour (cached per address and user).
|
||||||
|
// It tries the MariaDB client first (it speaks every MySQL authentication
|
||||||
|
// method), then the Oracle client.
|
||||||
|
func (e *Engine) server(ctx context.Context, v *config.Values, c Conn) (server, error) {
|
||||||
|
key := c.Addr() + "\x00" + c.User
|
||||||
|
e.mu.Lock()
|
||||||
|
s, ok := e.servers[key]
|
||||||
|
e.mu.Unlock()
|
||||||
|
if ok {
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
var cands []tool
|
||||||
|
if t, ok := e.maria(ctx, mariadbCLI, mysqlCLI); ok {
|
||||||
|
cands = append(cands, t)
|
||||||
|
}
|
||||||
|
if t, ok := e.oracle(ctx, mysqlCLI); ok {
|
||||||
|
cands = append(cands, t)
|
||||||
|
}
|
||||||
|
if len(cands) == 0 {
|
||||||
|
return s, errors.New("no mariadb or mysql client installed")
|
||||||
|
}
|
||||||
|
var firstErr error
|
||||||
|
for _, t := range cands {
|
||||||
|
out, err := query(ctx, v, c, t, "SELECT VERSION()")
|
||||||
|
if err != nil {
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s = server{version: strings.TrimSpace(out), mariadb: IsMariaDB(out)}
|
||||||
|
e.mu.Lock()
|
||||||
|
e.servers[key] = s
|
||||||
|
e.mu.Unlock()
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
return s, fmt.Errorf("failed to connect to database server %s: %w", c.Addr(), firstErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// systemDatabases are never backed up one by one (mysql-bkup 1.x list).
|
||||||
|
var systemDatabases = map[string]bool{
|
||||||
|
"information_schema": true, "performance_schema": true, "mysql": true, "sys": true, "innodb": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// listDatabases returns the user databases, minus exclude.
|
||||||
|
func (e *Engine) listDatabases(ctx context.Context, v *config.Values, c Conn, exclude []string) ([]string, error) {
|
||||||
|
srv, err := e.server(ctx, v, c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t, err := e.pick(ctx, v.String(SettingClient), srv, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out, err := query(ctx, v, c, t, "SHOW DATABASES")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to list databases: %w", err)
|
||||||
|
}
|
||||||
|
return filterDatabases(strings.Split(out, "\n"), exclude), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterDatabases(names, exclude []string) []string {
|
||||||
|
skip := map[string]bool{}
|
||||||
|
for _, x := range exclude {
|
||||||
|
skip[x] = true
|
||||||
|
}
|
||||||
|
var dbs []string
|
||||||
|
for _, n := range names {
|
||||||
|
n = strings.TrimRight(n, "\r")
|
||||||
|
if n == "" || systemDatabases[strings.ToLower(n)] || skip[n] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dbs = append(dbs, n)
|
||||||
|
}
|
||||||
|
return dbs
|
||||||
|
}
|
||||||
|
|
||||||
|
// quoteIdent quotes a MySQL identifier.
|
||||||
|
func quoteIdent(s string) string {
|
||||||
|
return "`" + strings.ReplaceAll(s, "`", "``") + "`"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDatabase creates db when it does not exist yet. The existence check
|
||||||
|
// runs first so that users without CREATE privilege can restore into an
|
||||||
|
// existing database.
|
||||||
|
func ensureDatabase(ctx context.Context, v *config.Values, c Conn, t tool, db string) error {
|
||||||
|
lit := "CONVERT(X'" + hex.EncodeToString([]byte(db)) + "' USING utf8mb4)"
|
||||||
|
out, err := query(ctx, v, c, t, "SELECT COUNT(*) FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = "+lit)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check database %s: %w", db, err)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(out) != "0" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
slog.Default().Info("Creating database", "database", db)
|
||||||
|
if _, err := query(ctx, v, c, t, "CREATE DATABASE IF NOT EXISTS "+quoteIdent(db)); err != nil {
|
||||||
|
return fmt.Errorf("create database %s: %w", db, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jkaninda/bkupkit/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TLS modes accepted in DB_SSL_MODE (MySQL --ssl-mode names, lower-case).
|
||||||
|
const (
|
||||||
|
SSLDisabled = "disabled"
|
||||||
|
SSLPreferred = "preferred"
|
||||||
|
SSLRequired = "required"
|
||||||
|
SSLVerifyCA = "verify_ca"
|
||||||
|
SSLVerifyIdentity = "verify_identity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseSSLMode normalizes DB_SSL_MODE. mysql-bkup 1.x wrote the value into
|
||||||
|
// my.cnf as "ssl=<value>", so "0"/"1" (and booleans) are still accepted.
|
||||||
|
// An empty value means disabled, the 1.x default.
|
||||||
|
func ParseSSLMode(s string) (string, error) {
|
||||||
|
m := strings.ToLower(strings.TrimSpace(s))
|
||||||
|
m = strings.ReplaceAll(m, "-", "_")
|
||||||
|
switch m {
|
||||||
|
case "", "0", "false", "off", "no", SSLDisabled, "disable":
|
||||||
|
return SSLDisabled, nil
|
||||||
|
case "1", "true", "on", "yes", SSLRequired, "require":
|
||||||
|
return SSLRequired, nil
|
||||||
|
case SSLPreferred, "prefer":
|
||||||
|
return SSLPreferred, nil
|
||||||
|
case SSLVerifyCA:
|
||||||
|
return SSLVerifyCA, nil
|
||||||
|
case SSLVerifyIdentity, "verify_full":
|
||||||
|
return SSLVerifyIdentity, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("invalid TLS mode %q: use disabled, preferred, required, verify_ca or verify_identity", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conn is a resolved connection.
|
||||||
|
type Conn struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
DB string
|
||||||
|
SSLMode string
|
||||||
|
SSLCA string
|
||||||
|
SSLCert string
|
||||||
|
SSLKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr is host:port (or the socket path), for logs and cache keys.
|
||||||
|
func (c Conn) Addr() string {
|
||||||
|
if strings.HasPrefix(c.Host, "/") {
|
||||||
|
return c.Host
|
||||||
|
}
|
||||||
|
return c.Host + ":" + strconv.Itoa(c.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseURL parses mysql://user:pass@host:port/db?ssl-mode=required (also
|
||||||
|
// mariadb://). Query parameters: ssl-mode (sslmode, tls), ssl-ca, ssl-cert, ssl-key.
|
||||||
|
func ParseURL(raw string) (Conn, error) {
|
||||||
|
var c Conn
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return c, errors.New("invalid database URL")
|
||||||
|
}
|
||||||
|
switch strings.ToLower(u.Scheme) {
|
||||||
|
case "mysql", "mariadb", "mysqlx":
|
||||||
|
default:
|
||||||
|
return c, fmt.Errorf("unsupported database URL scheme %q (use mysql://user:password@host:port/database)", u.Scheme)
|
||||||
|
}
|
||||||
|
c.Host = u.Hostname()
|
||||||
|
if p := u.Port(); p != "" {
|
||||||
|
n, err := strconv.Atoi(p)
|
||||||
|
if err != nil || n <= 0 || n > 65535 {
|
||||||
|
return c, fmt.Errorf("invalid port %q in database URL", p)
|
||||||
|
}
|
||||||
|
c.Port = n
|
||||||
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
c.User = u.User.Username()
|
||||||
|
c.Password, _ = u.User.Password()
|
||||||
|
}
|
||||||
|
c.DB = strings.TrimPrefix(u.Path, "/")
|
||||||
|
q := u.Query()
|
||||||
|
for _, k := range []string{"ssl-mode", "sslmode", "ssl_mode", "tls"} {
|
||||||
|
if v := q.Get(k); v != "" {
|
||||||
|
if c.SSLMode, err = ParseSSLMode(v); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.SSLCA = first(q.Get("ssl-ca"), q.Get("sslrootcert"))
|
||||||
|
c.SSLCert = first(q.Get("ssl-cert"), q.Get("sslcert"))
|
||||||
|
c.SSLKey = first(q.Get("ssl-key"), q.Get("sslkey"))
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func first(s ...string) string {
|
||||||
|
for _, v := range s {
|
||||||
|
if v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// connKeys names the settings of one side (source or migrate target).
|
||||||
|
type connKeys struct {
|
||||||
|
URL, Host, Port, Name, User, Pass, SSLMode, SSLCA, SSLCert, SSLKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sourceKeys = connKeys{config.DBURL, config.DBHost, config.DBPort, config.DBName, config.DBUser, config.DBPassword,
|
||||||
|
config.DBSSLMode, config.DBSSLCA, config.DBSSLCert, config.DBSSLKey}
|
||||||
|
targetKeys = connKeys{config.TargetDBURL, config.TargetDBHost, config.TargetDBPort, config.TargetDBName, config.TargetDBUser,
|
||||||
|
config.TargetDBPass, config.TargetDBSSL, config.TargetDBSSLCA, "", ""}
|
||||||
|
)
|
||||||
|
|
||||||
|
// connFrom resolves a connection: DB_URL first, then the individual settings
|
||||||
|
// (which also fill in what the URL leaves out).
|
||||||
|
func connFrom(v *config.Values, k connKeys) (Conn, error) {
|
||||||
|
var c Conn
|
||||||
|
if raw := v.String(k.URL); raw != "" {
|
||||||
|
var err error
|
||||||
|
if c, err = ParseURL(raw); err != nil {
|
||||||
|
return c, fmt.Errorf("%s: %w", k.URL, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str := func(dst *string, key string) {
|
||||||
|
if *dst == "" && key != "" {
|
||||||
|
*dst = v.String(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
str(&c.Host, k.Host)
|
||||||
|
str(&c.User, k.User)
|
||||||
|
str(&c.Password, k.Pass)
|
||||||
|
str(&c.DB, k.Name)
|
||||||
|
str(&c.SSLCA, k.SSLCA)
|
||||||
|
str(&c.SSLCert, k.SSLCert)
|
||||||
|
str(&c.SSLKey, k.SSLKey)
|
||||||
|
if c.Port == 0 {
|
||||||
|
c.Port = v.Int(k.Port)
|
||||||
|
}
|
||||||
|
if c.Port == 0 {
|
||||||
|
c.Port = 3306
|
||||||
|
}
|
||||||
|
if c.SSLMode == "" {
|
||||||
|
m, err := ParseSSLMode(v.String(k.SSLMode))
|
||||||
|
if err != nil {
|
||||||
|
return c, fmt.Errorf("%s: %w", k.SSLMode, err)
|
||||||
|
}
|
||||||
|
c.SSLMode = m
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SourceConn returns the DB_* connection of v.
|
||||||
|
func SourceConn(v *config.Values) (Conn, error) { return connFrom(v, sourceKeys) }
|
||||||
|
|
||||||
|
// TargetConn returns the TARGET_DB_* connection of v (migrate).
|
||||||
|
func TargetConn(v *config.Values) (Conn, error) {
|
||||||
|
c, err := connFrom(v, targetKeys)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
if v.String(config.TargetDBURL) == "" && v.String(config.TargetDBHost) == "" {
|
||||||
|
return c, errors.New("missing required configuration: TARGET_DB_HOST (or TARGET_DB_URL)")
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Conn) validate(prefix string) error {
|
||||||
|
var missing []string
|
||||||
|
if c.Host == "" {
|
||||||
|
missing = append(missing, prefix+"DB_HOST")
|
||||||
|
}
|
||||||
|
if c.User == "" {
|
||||||
|
missing = append(missing, prefix+"DB_USER")
|
||||||
|
}
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return fmt.Errorf("missing required configuration: %s", strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkDBName rejects names that a client would parse as an option.
|
||||||
|
func checkDBName(name string) error {
|
||||||
|
if strings.HasPrefix(name, "-") {
|
||||||
|
return fmt.Errorf("invalid database name %q", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,487 @@
|
|||||||
|
// Package mysql is the mysql-bkup engine for bkupkit: MySQL and MariaDB
|
||||||
|
// logical backups with mysqldump / mariadb-dump, restored with mysql / mariadb.
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/jkaninda/bkupkit/cli"
|
||||||
|
"github.com/jkaninda/bkupkit/config"
|
||||||
|
"github.com/jkaninda/bkupkit/engine"
|
||||||
|
"github.com/jkaninda/bkupkit/execx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine-specific settings.
|
||||||
|
const (
|
||||||
|
SettingClient = "MYSQL_CLIENT"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllInOneName is the target (and default artifact) name of --all-in-one backups.
|
||||||
|
const AllInOneName = "all_databases"
|
||||||
|
|
||||||
|
// DefaultCA is the 1.x default of DB_SSL_CA.
|
||||||
|
const DefaultCA = "/etc/ssl/certs/ca-certificates.crt"
|
||||||
|
|
||||||
|
// Engine implements engine.Engine and engine.Migrator.
|
||||||
|
type Engine struct {
|
||||||
|
// LookPath finds client binaries (default exec.LookPath); tests replace it.
|
||||||
|
LookPath func(string) (string, error)
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
tools map[string]tool
|
||||||
|
servers map[string]server
|
||||||
|
warned map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ engine.Engine = (*Engine)(nil)
|
||||||
|
_ engine.Migrator = (*Engine)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns the engine.
|
||||||
|
func New() *Engine {
|
||||||
|
return &Engine{tools: map[string]tool{}, servers: map[string]server{}, warned: map[string]bool{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool implements engine.Engine.
|
||||||
|
func (e *Engine) Tool() string { return "mysql-bkup" }
|
||||||
|
|
||||||
|
// Kind implements engine.Engine. It is "mysql" for MySQL and MariaDB alike so
|
||||||
|
// that {engine} in the index name does not depend on the server.
|
||||||
|
func (e *Engine) Kind() string { return "mysql" }
|
||||||
|
|
||||||
|
// Settings implements engine.Engine.
|
||||||
|
func (e *Engine) Settings(r *config.Registry) {
|
||||||
|
config.RegisterDatabase(r, "3306")
|
||||||
|
r.Override(config.DBURL, func(s *config.Setting) {
|
||||||
|
s.Usage = "Connection URL mysql://user:password@host:port/database?ssl-mode=required (overrides host, port, user, password)"
|
||||||
|
})
|
||||||
|
r.Override(config.DBHost, func(s *config.Setting) {
|
||||||
|
s.Usage = "Database host (a path starting with / is a Unix socket)"
|
||||||
|
})
|
||||||
|
r.Override(config.DBSSLMode, func(s *config.Setting) {
|
||||||
|
s.Default = SSLDisabled
|
||||||
|
s.Usage = "TLS mode: disabled, preferred, required, verify_ca, verify_identity (1.x values 0 and 1 mean disabled and required)"
|
||||||
|
})
|
||||||
|
r.Override(config.DBSSLCA, func(s *config.Setting) {
|
||||||
|
s.Default = DefaultCA
|
||||||
|
s.Usage = "CA certificate file, used with DB_SSL_MODE=verify_ca or verify_identity"
|
||||||
|
})
|
||||||
|
r.Override(config.TargetDBSSL, func(s *config.Setting) {
|
||||||
|
s.Default = SSLDisabled
|
||||||
|
s.Usage = "migrate: target TLS mode (same values as DB_SSL_MODE)"
|
||||||
|
})
|
||||||
|
r.Override(config.TargetDBSSLCA, func(s *config.Setting) {
|
||||||
|
s.Default = DefaultCA
|
||||||
|
})
|
||||||
|
r.Override(config.TargetDBName, func(s *config.Setting) {
|
||||||
|
s.Usage = "migrate: target database (default: the source name)"
|
||||||
|
})
|
||||||
|
// 1.x accepted --storage remote for SFTP.
|
||||||
|
r.Override(config.Storage, func(s *config.Setting) {
|
||||||
|
s.Enum = append(s.Enum, "remote")
|
||||||
|
s.Usage = "Storage backend: local, s3, gcs, azure, sftp (ssh, remote), ftp"
|
||||||
|
})
|
||||||
|
r.Add(config.Setting{
|
||||||
|
Key: SettingClient, Default: ClientAuto, Enum: []string{ClientAuto, ClientMySQL, ClientMariaDB},
|
||||||
|
Group: config.GroupDatabase,
|
||||||
|
Usage: "Client tools: auto (mariadb-dump for MariaDB, Oracle mysqldump for MySQL), mysql or mariadb",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure implements engine.Engine. It only validates values: commands that
|
||||||
|
// do not connect (list, prune, verify) must work without credentials.
|
||||||
|
func (e *Engine) Configure(_ context.Context, v *config.Values) error {
|
||||||
|
normalizeStorage(v)
|
||||||
|
var errs []error
|
||||||
|
if _, err := SourceConn(v); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
if v.Has(config.TargetDBURL) || v.Has(config.TargetDBHost) {
|
||||||
|
if _, err := TargetConn(v); err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeStorage maps the 1.x storage name "remote" to sftp, and an
|
||||||
|
// absolute local FILE_NAME (1.x: restore -f /backup/x.sql.gz) to a file name
|
||||||
|
// relative to STORAGE_PATH (or to its own directory, as 1.x did).
|
||||||
|
func normalizeStorage(v *config.Values) {
|
||||||
|
if v.String(config.Storage) == "remote" {
|
||||||
|
v.Set(config.Storage, "sftp")
|
||||||
|
}
|
||||||
|
f := v.String(config.FileName)
|
||||||
|
if v.String(config.Storage) != "local" || !filepath.IsAbs(f) || v.Has(config.RemotePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
root := filepath.Clean(v.String(config.StoragePath))
|
||||||
|
if rel, err := filepath.Rel(root, filepath.Clean(f)); err == nil && !strings.HasPrefix(rel, "..") {
|
||||||
|
v.Set(config.FileName, filepath.ToSlash(rel))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v.Set(config.StoragePath, filepath.Dir(f))
|
||||||
|
v.Set(config.FileName, filepath.Base(f))
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreContext reports whether v belongs to a restore/verify/delete run,
|
||||||
|
// where one database at most is involved. Outside the CLI (no command), a
|
||||||
|
// FILE_NAME means the same.
|
||||||
|
func restoreContext(v *config.Values) bool {
|
||||||
|
switch v.Command {
|
||||||
|
case "restore", "verify", "delete":
|
||||||
|
return true
|
||||||
|
case "":
|
||||||
|
return v.Has(config.FileName) && !v.Bool(config.AllDatabases) && !v.Bool(config.AllInOne)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets implements engine.Engine.
|
||||||
|
func (e *Engine) Targets(ctx context.Context, v *config.Values) ([]engine.Target, error) {
|
||||||
|
normalizeStorage(v) // per-job values of a config file are resolved again
|
||||||
|
c, err := SourceConn(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if restoreContext(v) {
|
||||||
|
// restore -f FILE [-d DB]: without a database, the dump's own USE
|
||||||
|
// statements decide (--all-in-one dumps).
|
||||||
|
if err := checkDBName(c.DB); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []engine.Target{{Name: c.DB, Values: v}}, nil
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case v.Bool(config.AllInOne):
|
||||||
|
return []engine.Target{{Name: AllInOneName, Values: v}}, nil
|
||||||
|
case v.Bool(config.AllDatabases):
|
||||||
|
if err := c.validate(""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dbs, err := e.listDatabases(ctx, v, c, v.List(config.ExcludeDBs))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(dbs) == 0 {
|
||||||
|
return nil, errors.New("no databases found")
|
||||||
|
}
|
||||||
|
ts := make([]engine.Target, 0, len(dbs))
|
||||||
|
for _, db := range dbs {
|
||||||
|
ts = append(ts, engine.Target{Name: db, Values: v})
|
||||||
|
}
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
// No database: fine for list, prune, verify and delete; Ping rejects it
|
||||||
|
// for a backup.
|
||||||
|
if err := checkDBName(c.DB); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return []engine.Target{{Name: c.DB, Values: v}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errNoDatabase is a configuration error (exit code 2).
|
||||||
|
var errNoDatabase = &cli.ExitError{Code: cli.ExitConfig, Err: errors.New("database name is required, use DB_NAME environment variable or -d flag")}
|
||||||
|
|
||||||
|
// Ext implements engine.Engine.
|
||||||
|
func (e *Engine) Ext(engine.Target) string { return ".sql" }
|
||||||
|
|
||||||
|
// NativeCompression implements engine.Engine.
|
||||||
|
func (e *Engine) NativeCompression(engine.Target) bool { return false }
|
||||||
|
|
||||||
|
// VerifyBeforeRestore makes RESTORE_VERIFY_FIRST=auto read and check the whole
|
||||||
|
// dump before the client runs: MySQL cannot roll back the dump's DROP/CREATE
|
||||||
|
// TABLE statements, so a backup that turns out to be damaged halfway would
|
||||||
|
// otherwise leave the database half-restored.
|
||||||
|
func (e *Engine) VerifyBeforeRestore(engine.Target, engine.ArtifactInfo) bool { return true }
|
||||||
|
|
||||||
|
var _ engine.RestoreVerifier = (*Engine)(nil)
|
||||||
|
|
||||||
|
// Ping implements engine.Engine: it connects, and detects the server flavour.
|
||||||
|
func (e *Engine) Ping(ctx context.Context, t engine.Target) error {
|
||||||
|
c, err := SourceConn(t.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t.Name == "" && !restoreContext(t.Values) {
|
||||||
|
return errNoDatabase
|
||||||
|
}
|
||||||
|
if err := c.validate(""); err != nil {
|
||||||
|
return &cli.ExitError{Code: cli.ExitConfig, Err: err}
|
||||||
|
}
|
||||||
|
srv, err := e.server(ctx, t.Values, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
flavour := "MySQL"
|
||||||
|
if srv.mariadb {
|
||||||
|
flavour = "MariaDB"
|
||||||
|
}
|
||||||
|
slog.Default().Info("Connected to database server", "server", c.Addr(), "flavour", flavour, "version", srv.version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerVersion implements engine.Engine.
|
||||||
|
func (e *Engine) ServerVersion(ctx context.Context, t engine.Target) string {
|
||||||
|
c, err := SourceConn(t.Values)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
srv, err := e.server(ctx, t.Values, c)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return srv.version
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumpSpec describes what a dump covers.
|
||||||
|
type dumpSpec struct {
|
||||||
|
db string // one database (no CREATE DATABASE / USE: restorable under another name)
|
||||||
|
all bool // --all-databases
|
||||||
|
databases []string // --databases list (all-in-one with exclusions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DumpArgs returns the dump arguments after --defaults-extra-file.
|
||||||
|
// mariadbTool is the client family, mariadbServer the server flavour.
|
||||||
|
func DumpArgs(mariadbTool, mariadbServer bool, s dumpSpec) []string {
|
||||||
|
args := []string{"--single-transaction", "--routines", "--triggers", "--events", "--hex-blob", "--quick"}
|
||||||
|
if !mariadbServer {
|
||||||
|
// MySQL: no tablespace statements (they need the PROCESS privilege
|
||||||
|
// and are useless in logical backups).
|
||||||
|
args = append(args, "--no-tablespaces")
|
||||||
|
if !mariadbTool {
|
||||||
|
args = append(args, "--set-gtid-purged=OFF")
|
||||||
|
}
|
||||||
|
} else if !mariadbTool {
|
||||||
|
// Oracle mysqldump against MariaDB: no column_statistics table there.
|
||||||
|
args = append(args, "--column-statistics=0")
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case s.all:
|
||||||
|
args = append(args, "--all-databases")
|
||||||
|
case len(s.databases) > 0:
|
||||||
|
args = append(args, "--databases")
|
||||||
|
args = append(args, s.databases...)
|
||||||
|
default:
|
||||||
|
args = append(args, s.db)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) dumpCmd(ctx context.Context, t engine.Target, c Conn) (tool, []string, error) {
|
||||||
|
v := t.Values
|
||||||
|
srv, err := e.server(ctx, v, c)
|
||||||
|
if err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
tl, err := e.pick(ctx, v.String(SettingClient), srv, true)
|
||||||
|
if err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
spec := dumpSpec{db: t.Name}
|
||||||
|
if t.Name == AllInOneName && v.Bool(config.AllInOne) {
|
||||||
|
spec = dumpSpec{all: true}
|
||||||
|
if ex := v.List(config.ExcludeDBs); len(ex) > 0 {
|
||||||
|
dbs, err := e.listDatabases(ctx, v, c, ex)
|
||||||
|
if err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
if len(dbs) == 0 {
|
||||||
|
return tool{}, nil, errors.New("no databases left after BACKUP_EXCLUDE_DATABASES")
|
||||||
|
}
|
||||||
|
spec = dumpSpec{databases: dbs}
|
||||||
|
}
|
||||||
|
} else if err := checkDBName(t.Name); err != nil || t.Name == "" {
|
||||||
|
return tool{}, nil, fmt.Errorf("invalid database name %q", t.Name)
|
||||||
|
}
|
||||||
|
return tl, DumpArgs(tl.mariadb, srv.mariadb, spec), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump implements engine.Engine: the dump goes straight to w.
|
||||||
|
func (e *Engine) Dump(ctx context.Context, t engine.Target, w io.Writer) error {
|
||||||
|
c, err := SourceConn(t.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tl, args, err := e.dumpCmd(ctx, t, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Default().Info("Dumping database", "database", t.Name, "client", tl.name)
|
||||||
|
return withOptionFile(t.Values, c, tl, func(cnf string) error {
|
||||||
|
return execx.Run(ctx, execx.Cmd{
|
||||||
|
Name: tl.path, Args: append([]string{"--defaults-extra-file=" + cnf}, args...),
|
||||||
|
Stdout: w, Log: slog.Default(), StderrLevel: slog.LevelWarn,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreDB returns the database a dump is replayed into ("" = none: the
|
||||||
|
// dump's USE statements decide).
|
||||||
|
func restoreDB(t engine.Target) string {
|
||||||
|
if t.Name == AllInOneName && t.Values.Bool(config.AllInOne) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) restoreCmd(ctx context.Context, v *config.Values, c Conn, db string) (tool, []string, error) {
|
||||||
|
if err := checkDBName(db); err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
srv, err := e.server(ctx, v, c)
|
||||||
|
if err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
tl, err := e.pick(ctx, v.String(SettingClient), srv, false)
|
||||||
|
if err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
if db != "" {
|
||||||
|
if err := ensureDatabase(ctx, v, c, tl, db); err != nil {
|
||||||
|
return tool{}, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --binary-mode: dumps may contain \0 and \r in string literals.
|
||||||
|
args := []string{"--binary-mode"}
|
||||||
|
if db != "" {
|
||||||
|
args = append(args, db)
|
||||||
|
}
|
||||||
|
return tl, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore implements engine.Engine. The client reads the dump from stdin and
|
||||||
|
// stops at the first failing statement (non-zero exit). If r fails, execx
|
||||||
|
// kills the client before it sees EOF. MySQL cannot roll back DDL, so a
|
||||||
|
// failed restore can leave the statements before the failure applied.
|
||||||
|
func (e *Engine) Restore(ctx context.Context, t engine.Target, r io.Reader, info engine.ArtifactInfo) error {
|
||||||
|
c, err := SourceConn(t.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
db := restoreDB(t)
|
||||||
|
tl, args, err := e.restoreCmd(ctx, t.Values, c, db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
slog.Default().Info("Restoring database", "database", db, "file", info.File, "client", tl.name)
|
||||||
|
return withOptionFile(t.Values, c, tl, func(cnf string) error {
|
||||||
|
return execx.Run(ctx, execx.Cmd{
|
||||||
|
Name: tl.path, Args: append([]string{"--defaults-extra-file=" + cnf}, args...),
|
||||||
|
Stdin: r, Log: slog.Default(), StderrLevel: slog.LevelWarn,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MigrateTargets implements engine.Migrator: DB_NAME → TARGET_DB_NAME (default
|
||||||
|
// the same name), or every database with --all-databases.
|
||||||
|
func (e *Engine) MigrateTargets(ctx context.Context, v *config.Values) ([][2]engine.Target, error) {
|
||||||
|
tc, err := TargetConn(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := tc.validate("TARGET_"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sc, err := SourceConn(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := sc.validate(""); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if v.Bool(config.AllDatabases) || v.Bool(config.AllInOne) {
|
||||||
|
dbs, err := e.listDatabases(ctx, v, sc, v.List(config.ExcludeDBs))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pairs := make([][2]engine.Target, 0, len(dbs))
|
||||||
|
for _, db := range dbs {
|
||||||
|
pairs = append(pairs, [2]engine.Target{{Name: db, Values: v}, {Name: db, Values: v}})
|
||||||
|
}
|
||||||
|
return pairs, nil
|
||||||
|
}
|
||||||
|
if sc.DB == "" {
|
||||||
|
return nil, errNoDatabase
|
||||||
|
}
|
||||||
|
name := tc.DB
|
||||||
|
if name == "" {
|
||||||
|
name = sc.DB
|
||||||
|
}
|
||||||
|
for _, n := range []string{sc.DB, name} {
|
||||||
|
if err := checkDBName(n); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [][2]engine.Target{{{Name: sc.DB, Values: v}, {Name: name, Values: v}}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate implements engine.Migrator: dump | restore through a pipe, no file.
|
||||||
|
func (e *Engine) Migrate(ctx context.Context, source, target engine.Target) error {
|
||||||
|
sc, err := SourceConn(source.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tc, err := TargetConn(target.Values)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dumpTool, dumpArgs, err := e.dumpCmd(ctx, source, sc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("source: %w", err)
|
||||||
|
}
|
||||||
|
restTool, restArgs, err := e.restoreCmd(ctx, target.Values, tc, target.Name)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("target: %w", err)
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
// The first failure is the cause; the other side then fails because of it.
|
||||||
|
var (
|
||||||
|
once sync.Once
|
||||||
|
cause error
|
||||||
|
)
|
||||||
|
fail := func(err error) {
|
||||||
|
once.Do(func() { cause = err })
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
err := withOptionFile(source.Values, sc, dumpTool, func(cnf string) error {
|
||||||
|
return execx.Run(ctx, execx.Cmd{
|
||||||
|
Name: dumpTool.path, Args: append([]string{"--defaults-extra-file=" + cnf}, dumpArgs...),
|
||||||
|
Stdout: pw, Log: slog.Default(), StderrLevel: slog.LevelWarn,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("dump %s: %w", source.Name, err)
|
||||||
|
fail(err)
|
||||||
|
// The restore side must never see a clean EOF.
|
||||||
|
_ = pw.CloseWithError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = pw.Close()
|
||||||
|
}()
|
||||||
|
err = withOptionFile(target.Values, tc, restTool, func(cnf string) error {
|
||||||
|
return execx.Run(ctx, execx.Cmd{
|
||||||
|
Name: restTool.path, Args: append([]string{"--defaults-extra-file=" + cnf}, restArgs...),
|
||||||
|
Stdin: pr, Log: slog.Default(), StderrLevel: slog.LevelWarn,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fail(fmt.Errorf("restore into %s: %w", target.Name, err))
|
||||||
|
_ = pr.CloseWithError(err)
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
return cause
|
||||||
|
}
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jkaninda/bkupkit/cli"
|
||||||
|
"github.com/jkaninda/bkupkit/config"
|
||||||
|
"github.com/jkaninda/bkupkit/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolve(t *testing.T, env config.MapEnv) *config.Values {
|
||||||
|
t.Helper()
|
||||||
|
r := config.NewRegistry()
|
||||||
|
config.RegisterCommon(r)
|
||||||
|
New().Settings(r)
|
||||||
|
v, err := r.Resolve(config.ResolveOptions{Env: env})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSSLMode(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"": SSLDisabled, "0": SSLDisabled, "false": SSLDisabled, "DISABLED": SSLDisabled,
|
||||||
|
"1": SSLRequired, "true": SSLRequired, "Required": SSLRequired,
|
||||||
|
"preferred": SSLPreferred, "VERIFY_CA": SSLVerifyCA, "verify-identity": SSLVerifyIdentity,
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
got, err := ParseSSLMode(in)
|
||||||
|
if err != nil || got != want {
|
||||||
|
t.Errorf("ParseSSLMode(%q) = %q, %v; want %q", in, got, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := ParseSSLMode("2"); err == nil {
|
||||||
|
t.Error("expected an error for 2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseURL(t *testing.T) {
|
||||||
|
c, err := ParseURL("mysql://bob:p%40ss%3Aw%2Frd@db.example:3307/shop?ssl-mode=verify_identity&ssl-ca=/ca.pem")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := Conn{Host: "db.example", Port: 3307, User: "bob", Password: "p@ss:w/rd", DB: "shop", SSLMode: SSLVerifyIdentity, SSLCA: "/ca.pem"}
|
||||||
|
if c != want {
|
||||||
|
t.Fatalf("got %+v\nwant %+v", c, want)
|
||||||
|
}
|
||||||
|
if _, err := ParseURL("postgres://a@b/c"); err == nil {
|
||||||
|
t.Fatal("expected scheme error")
|
||||||
|
}
|
||||||
|
if _, err := ParseURL("mysql://a@b:99999/c"); err == nil {
|
||||||
|
t.Fatal("expected port error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceConnPrecedence(t *testing.T) {
|
||||||
|
v := resolve(t, config.MapEnv{
|
||||||
|
"DB_URL": "mariadb://u:pw@h1/db1", "DB_HOST": "ignored", "DB_PORT": "3310", "DB_NAME": "ignored",
|
||||||
|
"DB_SSL_MODE": "1",
|
||||||
|
})
|
||||||
|
c, err := SourceConn(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.Host != "h1" || c.Port != 3310 || c.User != "u" || c.Password != "pw" || c.DB != "db1" || c.SSLMode != SSLRequired {
|
||||||
|
t.Fatalf("%+v", c)
|
||||||
|
}
|
||||||
|
if c.SSLCA != DefaultCA {
|
||||||
|
t.Fatalf("default CA = %q", c.SSLCA)
|
||||||
|
}
|
||||||
|
|
||||||
|
v = resolve(t, config.MapEnv{"DB_HOST": "h", "DB_USER": "legacy", "DB_PASSWORD": "x", "DB_NAME": "d"})
|
||||||
|
c, _ = SourceConn(v)
|
||||||
|
if c.User != "legacy" || c.Port != 3306 || c.SSLMode != SSLDisabled {
|
||||||
|
t.Fatalf("%+v", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
v = resolve(t, config.MapEnv{"DB_SSL_MODE": "sometimes"})
|
||||||
|
if err := New().Configure(context.Background(), v); err == nil {
|
||||||
|
t.Fatal("invalid DB_SSL_MODE accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetConn(t *testing.T) {
|
||||||
|
v := resolve(t, config.MapEnv{"DB_NAME": "a"})
|
||||||
|
if _, err := TargetConn(v); err == nil {
|
||||||
|
t.Fatal("expected missing TARGET_DB_HOST")
|
||||||
|
}
|
||||||
|
v = resolve(t, config.MapEnv{"TARGET_DB_HOST": "t", "TARGET_DB_USERNAME": "tu", "TARGET_DB_PASSWORD": "tp", "TARGET_DB_SSL_MODE": "verify_ca"})
|
||||||
|
c, err := TargetConn(v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c.Host != "t" || c.User != "tu" || c.Password != "tp" || c.Port != 3306 || c.SSLMode != SSLVerifyCA {
|
||||||
|
t.Fatalf("%+v", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuoteOption(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
`plain`: `"plain"`,
|
||||||
|
`a"b`: `"a\"b"`,
|
||||||
|
`back\slash`: `"back\\slash"`,
|
||||||
|
" # spaced ": `" # spaced "`,
|
||||||
|
"new\nline\t": `"new\nline\t"`,
|
||||||
|
`it's $x`: `"it's $x"`,
|
||||||
|
}
|
||||||
|
for in, want := range cases {
|
||||||
|
if got := quoteOption(in); got != want {
|
||||||
|
t.Errorf("quoteOption(%q) = %s, want %s", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptionFile(t *testing.T) {
|
||||||
|
c := Conn{Host: "db", Port: 3306, User: "root", Password: `p"w#`, SSLMode: SSLDisabled, SSLCA: DefaultCA}
|
||||||
|
got := optionFile(c, false)
|
||||||
|
for _, want := range []string{"[client]\n", "protocol=TCP\n", `host="db"` + "\n", "port=3306\n", `user="root"` + "\n",
|
||||||
|
`password="p\"w#"` + "\n", "ssl-mode=DISABLED\n", "get-server-public-key=1\n"} {
|
||||||
|
if !strings.Contains(got, want) {
|
||||||
|
t.Errorf("mysql option file lacks %q:\n%s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(got, "ssl-ca") {
|
||||||
|
t.Errorf("CA written without verification:\n%s", got)
|
||||||
|
}
|
||||||
|
got = optionFile(c, true)
|
||||||
|
if !strings.Contains(got, "ssl=0\n") || strings.Contains(got, "ssl-mode") || strings.Contains(got, "get-server-public-key") {
|
||||||
|
t.Errorf("mariadb option file:\n%s", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SSLMode = SSLVerifyIdentity
|
||||||
|
if got := optionFile(c, false); !strings.Contains(got, "ssl-mode=VERIFY_IDENTITY\n") || !strings.Contains(got, `ssl-ca="`+DefaultCA+`"`) {
|
||||||
|
t.Errorf("verify_identity:\n%s", got)
|
||||||
|
}
|
||||||
|
if got := optionFile(c, true); !strings.Contains(got, "ssl=1\nssl-verify-server-cert=1\n") || !strings.Contains(got, "ssl-ca=") {
|
||||||
|
t.Errorf("mariadb verify:\n%s", got)
|
||||||
|
}
|
||||||
|
c.SSLMode = SSLRequired
|
||||||
|
if got := optionFile(c, true); !strings.Contains(got, "ssl=1\nssl-verify-server-cert=0\n") || strings.Contains(got, "ssl-ca=") {
|
||||||
|
t.Errorf("mariadb required:\n%s", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Host = "/run/mysqld/mysqld.sock"
|
||||||
|
if got := optionFile(c, true); !strings.Contains(got, "protocol=SOCKET\n") || !strings.Contains(got, `socket="/run/mysqld/mysqld.sock"`) || strings.Contains(got, "host=") {
|
||||||
|
t.Errorf("socket:\n%s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDumpArgs(t *testing.T) {
|
||||||
|
base := []string{"--single-transaction", "--routines", "--triggers", "--events", "--hex-blob", "--quick"}
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
toolMaria, srvMaria bool
|
||||||
|
spec dumpSpec
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{"oracle-mysql", false, false, dumpSpec{db: "shop"}, append(append([]string{}, base...), "--no-tablespaces", "--set-gtid-purged=OFF", "shop")},
|
||||||
|
{"maria-maria", true, true, dumpSpec{db: "shop"}, append(append([]string{}, base...), "shop")},
|
||||||
|
{"maria-mysql", true, false, dumpSpec{all: true}, append(append([]string{}, base...), "--no-tablespaces", "--all-databases")},
|
||||||
|
{"oracle-maria", false, true, dumpSpec{databases: []string{"a", "b"}}, append(append([]string{}, base...), "--column-statistics=0", "--databases", "a", "b")},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := DumpArgs(c.toolMaria, c.srvMaria, c.spec); !reflect.DeepEqual(got, c.want) {
|
||||||
|
t.Errorf("%s:\n got %v\nwant %v", c.name, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterDatabases(t *testing.T) {
|
||||||
|
got := filterDatabases([]string{"information_schema", "shop", "mysql", "Performance_Schema", "sys", "blog", "", "tmp\r"}, []string{"tmp"})
|
||||||
|
if !reflect.DeepEqual(got, []string{"shop", "blog"}) {
|
||||||
|
t.Fatal(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsMariaDB(t *testing.T) {
|
||||||
|
if !IsMariaDB("11.8.3-MariaDB-ubu2404") || IsMariaDB("8.4.6") {
|
||||||
|
t.Fatal("flavour detection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuoteIdent(t *testing.T) {
|
||||||
|
if got := quoteIdent("we`ird"); got != "`we``ird`" {
|
||||||
|
t.Fatal(got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargets(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, c := range []struct{ storagePath, file, wantRoot, wantFile string }{
|
||||||
|
{"/backup", "/backup/x.sql.gz", "/backup", "x.sql.gz"},
|
||||||
|
{"/backup", "/backup/sub/x.sql.gz", "/backup", "sub/x.sql.gz"},
|
||||||
|
{"/backup", "/data/x.sql", "/data", "x.sql"},
|
||||||
|
{"/backup", "x.sql", "/backup", "x.sql"},
|
||||||
|
} {
|
||||||
|
v := resolve(t, config.MapEnv{"STORAGE_PATH": c.storagePath, "FILE_NAME": c.file, "DB_NAME": "x"})
|
||||||
|
if _, err := e.Targets(ctx, v); err != nil || v.String(config.StoragePath) != c.wantRoot || v.String(config.FileName) != c.wantFile {
|
||||||
|
t.Errorf("%s: %q %q %v", c.file, v.String(config.StoragePath), v.String(config.FileName), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v0 := resolve(t, config.MapEnv{"STORAGE": "Remote", "DB_NAME": "x"})
|
||||||
|
if _, err := e.Targets(ctx, v0); err != nil || v0.String(config.Storage) != "sftp" {
|
||||||
|
t.Fatalf("storage remote: %q %v", v0.String(config.Storage), err)
|
||||||
|
}
|
||||||
|
v := resolve(t, config.MapEnv{"DB_NAME": "shop"})
|
||||||
|
ts, err := e.Targets(ctx, v)
|
||||||
|
if err != nil || len(ts) != 1 || ts[0].Name != "shop" {
|
||||||
|
t.Fatalf("%v %v", ts, err)
|
||||||
|
}
|
||||||
|
v = resolve(t, config.MapEnv{})
|
||||||
|
ts, err = e.Targets(ctx, v)
|
||||||
|
if err != nil || len(ts) != 1 || ts[0].Name != "" {
|
||||||
|
t.Fatalf("no name: %v %v", ts, err)
|
||||||
|
}
|
||||||
|
var ee *cli.ExitError
|
||||||
|
if err := e.Ping(ctx, ts[0]); !errors.As(err, &ee) || ee.Code != cli.ExitConfig || !strings.Contains(err.Error(), "DB_NAME") {
|
||||||
|
t.Fatalf("backup without a database: %v", err)
|
||||||
|
}
|
||||||
|
// restore without a database (all-in-one dumps carry their own USE).
|
||||||
|
v = resolve(t, config.MapEnv{"FILE_NAME": "all_databases_20260101_000000.sql.gz"})
|
||||||
|
if ts, err := e.Targets(ctx, v); err != nil || ts[0].Name != "" {
|
||||||
|
t.Fatalf("%v %v", ts, err)
|
||||||
|
}
|
||||||
|
v = resolve(t, config.MapEnv{"BACKUP_ALL_IN_ONE": "true"})
|
||||||
|
if ts, err := e.Targets(ctx, v); err != nil || ts[0].Name != AllInOneName || restoreDB(ts[0]) != "" {
|
||||||
|
t.Fatalf("%v %v", ts, err)
|
||||||
|
}
|
||||||
|
v = resolve(t, config.MapEnv{"DB_NAME": "-x"})
|
||||||
|
if _, err := e.Targets(ctx, v); err == nil {
|
||||||
|
t.Fatal("option-like database name accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fakeClients installs shell scripts standing in for the client binaries.
|
||||||
|
// They log their argv and the option file, and answer the queries the engine runs.
|
||||||
|
func fakeClients(t *testing.T, serverVersion string, oracle bool) (dir, logFile string) {
|
||||||
|
t.Helper()
|
||||||
|
dir = t.TempDir()
|
||||||
|
logFile = filepath.Join(dir, "calls.log")
|
||||||
|
script := `#!/bin/sh
|
||||||
|
log="` + logFile + `"
|
||||||
|
echo "ARGV $(basename "$0") $*" >> "$log"
|
||||||
|
case "$1" in --defaults-extra-file=*) f="${1#--defaults-extra-file=}"; echo "MODE $(ls -ln "$f" | cut -c1-10)" >> "$log"; sed 's/^/CNF /' "$f" >> "$log";; esac
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in
|
||||||
|
--version) echo "$(basename "$0") Ver 8.4.6 for Linux (MySQL Community Server - GPL)"; exit 0;;
|
||||||
|
"SELECT VERSION()") echo "` + serverVersion + `"; exit 0;;
|
||||||
|
"SHOW DATABASES") printf 'information_schema\nmysql\nshop\nblog\nsys\n'; exit 0;;
|
||||||
|
SELECT\ COUNT*) echo 0; exit 0;;
|
||||||
|
CREATE\ DATABASE*) exit 0;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
case "$(basename "$0")" in
|
||||||
|
*dump) echo "-- dump of $*";;
|
||||||
|
*) cat > "` + filepath.Join(dir, "restored.sql") + `";;
|
||||||
|
esac
|
||||||
|
`
|
||||||
|
names := []string{"mariadb", "mariadb-dump"}
|
||||||
|
if oracle {
|
||||||
|
names = append(names, "mysql", "mysqldump")
|
||||||
|
}
|
||||||
|
for _, n := range names {
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, n), []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dir, logFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func engineWith(dir string) *Engine {
|
||||||
|
e := New()
|
||||||
|
e.LookPath = func(name string) (string, error) {
|
||||||
|
p := filepath.Join(dir, name)
|
||||||
|
if _, err := os.Stat(p); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDumpAndRestoreUseOptionFile(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, version string
|
||||||
|
oracle bool
|
||||||
|
wantDump, wantCL string
|
||||||
|
}{
|
||||||
|
{"mysql", "8.4.6", true, "mysqldump", "mysql"},
|
||||||
|
{"mariadb", "11.8.3-MariaDB", true, "mariadb-dump", "mariadb"},
|
||||||
|
{"mysql-without-oracle-client", "8.4.6", false, "mariadb-dump", "mariadb"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dir, logFile := fakeClients(t, tc.version, tc.oracle)
|
||||||
|
tmp := t.TempDir()
|
||||||
|
v := resolve(t, config.MapEnv{"DB_HOST": "db", "DB_USERNAME": "root", "DB_PASSWORD": "s3cr3t pw", "DB_NAME": "shop", "TMP_DIR": tmp})
|
||||||
|
e := engineWith(dir)
|
||||||
|
tg := engine.Target{Name: "shop", Values: v}
|
||||||
|
if err := e.Ping(ctx, tg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := e.ServerVersion(ctx, tg); got != tc.version {
|
||||||
|
t.Fatalf("version %q", got)
|
||||||
|
}
|
||||||
|
var out bytes.Buffer
|
||||||
|
if err := e.Dump(ctx, tg, &out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(out.String(), "-- dump of --defaults-extra-file=") || !strings.HasSuffix(strings.TrimSpace(out.String()), " shop") {
|
||||||
|
t.Fatalf("dump output %q", out.String())
|
||||||
|
}
|
||||||
|
if err := e.Restore(ctx, tg, strings.NewReader("CREATE TABLE t (id int);\n"), engine.ArtifactInfo{File: "x.sql"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b, _ := os.ReadFile(logFile)
|
||||||
|
calls := string(b)
|
||||||
|
if strings.Contains(strings.Join(argvLines(calls), "\n"), "s3cr3t") {
|
||||||
|
t.Fatalf("password in argv:\n%s", calls)
|
||||||
|
}
|
||||||
|
if !strings.Contains(calls, `CNF password="s3cr3t pw"`) {
|
||||||
|
t.Fatalf("password not in option file:\n%s", calls)
|
||||||
|
}
|
||||||
|
if !strings.Contains(calls, "MODE -rw-------") || strings.Contains(calls, "MODE -rw-r") {
|
||||||
|
t.Fatalf("option file mode:\n%s", calls)
|
||||||
|
}
|
||||||
|
for _, l := range argvLines(calls) {
|
||||||
|
f := strings.Fields(l)
|
||||||
|
if len(f) < 3 || !strings.HasPrefix(f[2], "--defaults-extra-file=") {
|
||||||
|
t.Fatalf("--defaults-extra-file is not the first argument: %q", l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.Contains(calls, "ARGV "+tc.wantDump+" ") || !strings.Contains(calls, "ARGV "+tc.wantCL+" --defaults-extra-file=") {
|
||||||
|
t.Fatalf("clients:\n%s", calls)
|
||||||
|
}
|
||||||
|
if !strings.Contains(calls, "--binary-mode shop") {
|
||||||
|
t.Fatalf("restore args:\n%s", calls)
|
||||||
|
}
|
||||||
|
if r, _ := os.ReadFile(filepath.Join(dir, "restored.sql")); string(r) != "CREATE TABLE t (id int);\n" {
|
||||||
|
t.Fatalf("restored %q", r)
|
||||||
|
}
|
||||||
|
if ents, _ := os.ReadDir(tmp); len(ents) != 0 {
|
||||||
|
t.Fatalf("TMP_DIR not cleaned: %v", ents)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func argvLines(log string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, l := range strings.Split(log, "\n") {
|
||||||
|
if strings.HasPrefix(l, "ARGV ") && !strings.Contains(l, "--version") {
|
||||||
|
out = append(out, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllDatabasesTargets(t *testing.T) {
|
||||||
|
dir, _ := fakeClients(t, "8.4.6", true)
|
||||||
|
v := resolve(t, config.MapEnv{"DB_HOST": "db", "DB_USERNAME": "root", "BACKUP_ALL_DATABASES": "1", "BACKUP_EXCLUDE_DATABASES": "blog", "TMP_DIR": t.TempDir()})
|
||||||
|
ts, err := engineWith(dir).Targets(context.Background(), v)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(ts) != 1 || ts[0].Name != "shop" {
|
||||||
|
t.Fatalf("%v", ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigratePipes(t *testing.T) {
|
||||||
|
dir, logFile := fakeClients(t, "11.8.3-MariaDB", true)
|
||||||
|
v := resolve(t, config.MapEnv{"DB_HOST": "src", "DB_USERNAME": "root", "DB_PASSWORD": "a", "DB_NAME": "shop",
|
||||||
|
"TARGET_DB_HOST": "dst", "TARGET_DB_USERNAME": "root", "TARGET_DB_PASSWORD": "b", "TARGET_DB_NAME": "shop2", "TMP_DIR": t.TempDir()})
|
||||||
|
e := engineWith(dir)
|
||||||
|
pairs, err := e.MigrateTargets(context.Background(), v)
|
||||||
|
if err != nil || len(pairs) != 1 || pairs[0][0].Name != "shop" || pairs[0][1].Name != "shop2" {
|
||||||
|
t.Fatalf("%v %v", pairs, err)
|
||||||
|
}
|
||||||
|
if err := e.Migrate(context.Background(), pairs[0][0], pairs[0][1]); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r, _ := os.ReadFile(filepath.Join(dir, "restored.sql"))
|
||||||
|
if !strings.HasPrefix(string(r), "-- dump of --defaults-extra-file=") {
|
||||||
|
t.Fatalf("restored %q", r)
|
||||||
|
}
|
||||||
|
b, _ := os.ReadFile(logFile)
|
||||||
|
if !strings.Contains(string(b), `CNF host="dst"`) || !strings.Contains(string(b), "--binary-mode shop2") {
|
||||||
|
t.Fatalf("calls:\n%s", b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifiesBeforeRestore(t *testing.T) {
|
||||||
|
// MySQL cannot roll back DDL: a damaged dump must be caught first.
|
||||||
|
if !New().VerifyBeforeRestore(engine.Target{}, engine.ArtifactInfo{}) {
|
||||||
|
t.Fatal("restores must verify the dump first")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// quoteOption quotes a value for a MySQL/MariaDB option file. Both client
|
||||||
|
// families parse option files with the same mysys code: a value wrapped in
|
||||||
|
// double quotes keeps leading/trailing spaces and '#', and the escapes
|
||||||
|
// \\ \" \n \r \t are decoded inside it.
|
||||||
|
func quoteOption(s string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(s) + 2)
|
||||||
|
b.WriteByte('"')
|
||||||
|
for _, r := range s {
|
||||||
|
switch r {
|
||||||
|
case '\\':
|
||||||
|
b.WriteString(`\\`)
|
||||||
|
case '"':
|
||||||
|
b.WriteString(`\"`)
|
||||||
|
case '\n':
|
||||||
|
b.WriteString(`\n`)
|
||||||
|
case '\r':
|
||||||
|
b.WriteString(`\r`)
|
||||||
|
case '\t':
|
||||||
|
b.WriteString(`\t`)
|
||||||
|
default:
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteByte('"')
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// optionFile returns the [client] option file for c. mariadbClient selects
|
||||||
|
// the MariaDB client option names (ssl, ssl-verify-server-cert) instead of
|
||||||
|
// the Oracle MySQL ones (ssl-mode, get-server-public-key).
|
||||||
|
//
|
||||||
|
// The file holds the password; it is written 0600 in a private directory and
|
||||||
|
// passed as --defaults-extra-file, never on the command line.
|
||||||
|
func optionFile(c Conn, mariadbClient bool) string {
|
||||||
|
var b strings.Builder
|
||||||
|
line := func(k, v string) {
|
||||||
|
b.WriteString(k)
|
||||||
|
b.WriteByte('=')
|
||||||
|
b.WriteString(v)
|
||||||
|
b.WriteByte('\n')
|
||||||
|
}
|
||||||
|
b.WriteString("[client]\n")
|
||||||
|
if strings.HasPrefix(c.Host, "/") {
|
||||||
|
line("protocol", "SOCKET")
|
||||||
|
line("socket", quoteOption(c.Host))
|
||||||
|
} else {
|
||||||
|
// TCP even for "localhost" (a client would otherwise look for a socket).
|
||||||
|
line("protocol", "TCP")
|
||||||
|
line("host", quoteOption(c.Host))
|
||||||
|
line("port", strconv.Itoa(c.Port))
|
||||||
|
}
|
||||||
|
line("user", quoteOption(c.User))
|
||||||
|
line("password", quoteOption(c.Password))
|
||||||
|
line("max-allowed-packet", "1G")
|
||||||
|
|
||||||
|
verify := c.SSLMode == SSLVerifyCA || c.SSLMode == SSLVerifyIdentity
|
||||||
|
if mariadbClient {
|
||||||
|
switch c.SSLMode {
|
||||||
|
case SSLDisabled:
|
||||||
|
line("ssl", "0")
|
||||||
|
case SSLPreferred:
|
||||||
|
// Leave TLS to the client default, without certificate checks.
|
||||||
|
line("ssl-verify-server-cert", "0")
|
||||||
|
case SSLRequired:
|
||||||
|
line("ssl", "1")
|
||||||
|
line("ssl-verify-server-cert", "0")
|
||||||
|
default: // verify_ca, verify_identity: MariaDB always checks the host name too
|
||||||
|
line("ssl", "1")
|
||||||
|
line("ssl-verify-server-cert", "1")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
line("ssl-mode", strings.ToUpper(c.SSLMode))
|
||||||
|
if c.SSLMode != SSLVerifyIdentity && c.SSLMode != SSLVerifyCA {
|
||||||
|
// caching_sha2_password over a non-TLS connection needs the
|
||||||
|
// server's RSA key (what the MariaDB client does by itself).
|
||||||
|
line("get-server-public-key", "1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.SSLMode != SSLDisabled {
|
||||||
|
// The CA is only used when certificates are verified: with the MySQL
|
||||||
|
// client, a CA would otherwise silently turn "required" into "verify_ca".
|
||||||
|
if verify && c.SSLCA != "" {
|
||||||
|
line("ssl-ca", quoteOption(c.SSLCA))
|
||||||
|
}
|
||||||
|
if c.SSLCert != "" {
|
||||||
|
line("ssl-cert", quoteOption(c.SSLCert))
|
||||||
|
}
|
||||||
|
if c.SSLKey != "" {
|
||||||
|
line("ssl-key", quoteOption(c.SSLKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
|
// Command mysql-bkup backs up and restores MySQL and MariaDB databases to
|
||||||
|
// local storage, S3, GCS, Azure Blob, SFTP or FTP, with optional GPG or age
|
||||||
|
// encryption. See https://github.com/jkaninda/mysql-bkup.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
//main
|
import (
|
||||||
/*****
|
"github.com/jkaninda/bkupkit/cli"
|
||||||
* MySQL Backup & Restore
|
"github.com/jkaninda/mysql-bkup/internal/mysql"
|
||||||
* @author Jonas Kaninda
|
)
|
||||||
* @license MIT License <https://opensource.org/licenses/MIT>
|
|
||||||
* @link https://github.com/jkaninda/mysql-bkup
|
var Version = "development"
|
||||||
**/
|
|
||||||
import "github.com/jkaninda/mysql-bkup/cmd"
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
(&cli.App{
|
||||||
cmd.Execute()
|
Engine: mysql.New(),
|
||||||
|
Version: Version,
|
||||||
|
Short: "Back up and restore MySQL and MariaDB databases (local, S3, GCS, Azure, SFTP, FTP; GPG or age encryption)",
|
||||||
|
}).Main()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
-- Create the database testdb2 and testdb3
|
||||||
|
CREATE DATABASE IF NOT EXISTS testdb2;
|
||||||
|
CREATE DATABASE IF NOT EXISTS testdb3;
|
||||||
|
CREATE DATABASE IF NOT EXISTS fakedb;
|
||||||
|
USE testdb;
|
||||||
|
|
||||||
|
-- Create the 'users' table
|
||||||
|
CREATE TABLE users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create the 'orders' table
|
||||||
|
CREATE TABLE orders (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
amount DECIMAL(10,2) NOT NULL,
|
||||||
|
status ENUM('pending', 'completed', 'canceled') NOT NULL DEFAULT 'pending',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert fake users
|
||||||
|
INSERT INTO users (name, email) VALUES
|
||||||
|
('Alice Smith', 'alice@example.com'),
|
||||||
|
('Bob Johnson', 'bob@example.com'),
|
||||||
|
('Charlie Brown', 'charlie@example.com');
|
||||||
|
|
||||||
|
-- Insert fake orders
|
||||||
|
INSERT INTO orders (user_id, amount, status) VALUES
|
||||||
|
(1, 100.50, 'completed'),
|
||||||
|
(2, 200.75, 'pending'),
|
||||||
|
(3, 50.00, 'canceled');
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#cronExpression: "@every 20s"
|
||||||
|
#backupRescueMode: false
|
||||||
|
databases:
|
||||||
|
- host: 127.0.0.1
|
||||||
|
port: 3306
|
||||||
|
name: testdb
|
||||||
|
user: user
|
||||||
|
password: password
|
||||||
|
- name: testdb2
|
||||||
|
# database credentials from environment variables
|
||||||
|
#TESTDB2_DB_USER
|
||||||
|
#TESTDB2_DB_PASSWORD
|
||||||
|
#TESTDB2_DB_HOST
|
||||||
-281
@@ -1,281 +0,0 @@
|
|||||||
// Package pkg /*
|
|
||||||
/*
|
|
||||||
Copyright © 2024 Jonas Kaninda
|
|
||||||
*/
|
|
||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/hpcloud/tail"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StartBackup(cmd *cobra.Command) {
|
|
||||||
_, _ = cmd.Flags().GetString("operation")
|
|
||||||
//Set env
|
|
||||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
|
||||||
utils.GetEnv(cmd, "dbname", "DB_NAME")
|
|
||||||
utils.GetEnv(cmd, "port", "DB_PORT")
|
|
||||||
utils.GetEnv(cmd, "period", "SCHEDULE_PERIOD")
|
|
||||||
|
|
||||||
//Get flag value and set env
|
|
||||||
remotePath := utils.GetEnv(cmd, "path", "SSH_REMOTE_PATH")
|
|
||||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
|
||||||
file = utils.GetEnv(cmd, "file", "FILE_NAME")
|
|
||||||
backupRetention, _ := cmd.Flags().GetInt("keep-last")
|
|
||||||
prune, _ := cmd.Flags().GetBool("prune")
|
|
||||||
disableCompression, _ = cmd.Flags().GetBool("disable-compression")
|
|
||||||
executionMode, _ = cmd.Flags().GetString("mode")
|
|
||||||
dbName = os.Getenv("DB_NAME")
|
|
||||||
gpqPassphrase := os.Getenv("GPG_PASSPHRASE")
|
|
||||||
_ = utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
|
||||||
|
|
||||||
//
|
|
||||||
if gpqPassphrase != "" {
|
|
||||||
encryption = true
|
|
||||||
}
|
|
||||||
|
|
||||||
//Generate file name
|
|
||||||
backupFileName := fmt.Sprintf("%s_%s.sql.gz", dbName, time.Now().Format("20060102_150405"))
|
|
||||||
if disableCompression {
|
|
||||||
backupFileName = fmt.Sprintf("%s_%s.sql", dbName, time.Now().Format("20060102_150405"))
|
|
||||||
}
|
|
||||||
|
|
||||||
if executionMode == "default" {
|
|
||||||
switch storage {
|
|
||||||
case "s3":
|
|
||||||
s3Backup(backupFileName, disableCompression, prune, backupRetention, encryption)
|
|
||||||
case "local":
|
|
||||||
localBackup(backupFileName, disableCompression, prune, backupRetention, encryption)
|
|
||||||
case "ssh", "remote":
|
|
||||||
sshBackup(backupFileName, remotePath, disableCompression, prune, backupRetention, encryption)
|
|
||||||
case "ftp":
|
|
||||||
utils.Fatal("Not supported storage type: %s", storage)
|
|
||||||
default:
|
|
||||||
localBackup(backupFileName, disableCompression, prune, backupRetention, encryption)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if executionMode == "scheduled" {
|
|
||||||
scheduledMode(storage)
|
|
||||||
} else {
|
|
||||||
utils.Fatal("Error, unknown execution mode!")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run in scheduled mode
|
|
||||||
func scheduledMode(storage string) {
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("**********************************")
|
|
||||||
fmt.Println(" Starting MySQL Bkup... ")
|
|
||||||
fmt.Println("***********************************")
|
|
||||||
utils.Info("Running in Scheduled mode")
|
|
||||||
utils.Info("Execution period %s", os.Getenv("SCHEDULE_PERIOD"))
|
|
||||||
utils.Info("Storage type %s ", storage)
|
|
||||||
|
|
||||||
//Test database connexion
|
|
||||||
utils.TestDatabaseConnection()
|
|
||||||
|
|
||||||
utils.Info("Creating backup job...")
|
|
||||||
CreateCrontabScript(disableCompression, storage)
|
|
||||||
|
|
||||||
supervisorConfig := "/etc/supervisor/supervisord.conf"
|
|
||||||
|
|
||||||
// Start Supervisor
|
|
||||||
cmd := exec.Command("supervisord", "-c", supervisorConfig)
|
|
||||||
err := cmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Failed to start supervisord: %v", err))
|
|
||||||
}
|
|
||||||
utils.Info("Backup job started")
|
|
||||||
defer func() {
|
|
||||||
if err := cmd.Process.Kill(); err != nil {
|
|
||||||
utils.Info("Failed to kill supervisord process: %v", err)
|
|
||||||
} else {
|
|
||||||
utils.Info("Supervisor stopped.")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if _, err := os.Stat(cronLogFile); os.IsNotExist(err) {
|
|
||||||
utils.Fatal(fmt.Sprintf("Log file %s does not exist.", cronLogFile))
|
|
||||||
}
|
|
||||||
t, err := tail.TailFile(cronLogFile, tail.Config{Follow: true})
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Failed to tail file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read and print new lines from the log file
|
|
||||||
for line := range t.Lines {
|
|
||||||
fmt.Println(line.Text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackupDatabase backup database
|
|
||||||
func BackupDatabase(backupFileName string, disableCompression bool) {
|
|
||||||
dbHost = os.Getenv("DB_HOST")
|
|
||||||
dbPassword = os.Getenv("DB_PASSWORD")
|
|
||||||
dbUserName = os.Getenv("DB_USERNAME")
|
|
||||||
dbName = os.Getenv("DB_NAME")
|
|
||||||
dbPort = os.Getenv("DB_PORT")
|
|
||||||
storagePath = os.Getenv("STORAGE_PATH")
|
|
||||||
|
|
||||||
err := utils.CheckEnvVars(dbHVars)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Please make sure all required environment variables for database are set")
|
|
||||||
utils.Fatal("Error checking environment variables: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.Info("Starting database backup...")
|
|
||||||
utils.TestDatabaseConnection()
|
|
||||||
|
|
||||||
// Backup Database database
|
|
||||||
utils.Info("Backing up database...")
|
|
||||||
|
|
||||||
if disableCompression {
|
|
||||||
// Execute mysqldump
|
|
||||||
cmd := exec.Command("mysqldump",
|
|
||||||
"-h", dbHost,
|
|
||||||
"-P", dbPort,
|
|
||||||
"-u", dbUserName,
|
|
||||||
"--password="+dbPassword,
|
|
||||||
dbName,
|
|
||||||
)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// save output
|
|
||||||
file, err := os.Create(fmt.Sprintf("%s/%s", tmpPath, backupFileName))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = file.Write(output)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
utils.Done("Database has been backed up")
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Execute mysqldump
|
|
||||||
cmd := exec.Command("mysqldump", "-h", dbHost, "-P", dbPort, "-u", dbUserName, "--password="+dbPassword, dbName)
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
gzipCmd := exec.Command("gzip")
|
|
||||||
gzipCmd.Stdin = stdout
|
|
||||||
gzipCmd.Stdout, err = os.Create(fmt.Sprintf("%s/%s", tmpPath, backupFileName))
|
|
||||||
gzipCmd.Start()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := gzipCmd.Wait(); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
utils.Done("Database has been backed up")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func localBackup(backupFileName string, disableCompression bool, prune bool, backupRetention int, encrypt bool) {
|
|
||||||
utils.Info("Backup database to local storage")
|
|
||||||
BackupDatabase(backupFileName, disableCompression)
|
|
||||||
finalFileName := backupFileName
|
|
||||||
if encrypt {
|
|
||||||
encryptBackup(backupFileName)
|
|
||||||
finalFileName = fmt.Sprintf("%s.%s", backupFileName, gpgExtension)
|
|
||||||
}
|
|
||||||
utils.Info("Backup name is %s", finalFileName)
|
|
||||||
moveToBackup(finalFileName, storagePath)
|
|
||||||
//Delete old backup
|
|
||||||
if prune {
|
|
||||||
deleteOldBackup(backupRetention)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func s3Backup(backupFileName string, disableCompression bool, prune bool, backupRetention int, encrypt bool) {
|
|
||||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
|
||||||
s3Path := utils.GetEnvVariable("AWS_S3_PATH", "S3_PATH")
|
|
||||||
utils.Info("Backup database to s3 storage")
|
|
||||||
//Backup database
|
|
||||||
BackupDatabase(backupFileName, disableCompression)
|
|
||||||
finalFileName := backupFileName
|
|
||||||
if encrypt {
|
|
||||||
encryptBackup(backupFileName)
|
|
||||||
finalFileName = fmt.Sprintf("%s.%s", backupFileName, "gpg")
|
|
||||||
}
|
|
||||||
utils.Info("Uploading backup file to S3 storage...")
|
|
||||||
utils.Info("Backup name is %s", finalFileName)
|
|
||||||
err := utils.UploadFileToS3(tmpPath, finalFileName, bucket, s3Path)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error uploading file to S3: %s ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delete backup file from tmp folder
|
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, backupFileName))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error deleting file: ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
// Delete old backup
|
|
||||||
if prune {
|
|
||||||
err := utils.DeleteOldBackup(bucket, s3Path, backupRetention)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error deleting old backup from S3: %s ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
utils.Done("Database has been backed up and uploaded to s3 ")
|
|
||||||
}
|
|
||||||
func sshBackup(backupFileName, remotePath string, disableCompression bool, prune bool, backupRetention int, encrypt bool) {
|
|
||||||
utils.Info("Backup database to Remote server")
|
|
||||||
//Backup database
|
|
||||||
BackupDatabase(backupFileName, disableCompression)
|
|
||||||
finalFileName := backupFileName
|
|
||||||
if encrypt {
|
|
||||||
encryptBackup(backupFileName)
|
|
||||||
finalFileName = fmt.Sprintf("%s.%s", backupFileName, "gpg")
|
|
||||||
}
|
|
||||||
utils.Info("Uploading backup file to remote server...")
|
|
||||||
utils.Info("Backup name is %s", finalFileName)
|
|
||||||
err := CopyToRemote(finalFileName, remotePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error uploading file to the remote server: %s ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//Delete backup file from tmp folder
|
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error deleting file: ", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
if prune {
|
|
||||||
//TODO: Delete old backup from remote server
|
|
||||||
utils.Info("Deleting old backup from a remote server is not implemented yet")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.Done("Database has been backed up and uploaded to remote server ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptBackup(backupFileName string) {
|
|
||||||
gpgPassphrase := os.Getenv("GPG_PASSPHRASE")
|
|
||||||
err := Encrypt(filepath.Join(tmpPath, backupFileName), gpgPassphrase)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error during encrypting backup %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Decrypt(inputFile string, passphrase string) error {
|
|
||||||
utils.Info("Decrypting backup file: " + inputFile + " ...")
|
|
||||||
cmd := exec.Command("gpg", "--batch", "--passphrase", passphrase, "--output", RemoveLastExtension(inputFile), "--decrypt", inputFile)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.Info("Backup file decrypted successful!")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Encrypt(inputFile string, passphrase string) error {
|
|
||||||
utils.Info("Encrypting backup...")
|
|
||||||
cmd := exec.Command("gpg", "--batch", "--passphrase", passphrase, "--symmetric", "--cipher-algo", algorithm, inputFile)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.Info("Backup file encrypted successful!")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveLastExtension(filename string) string {
|
|
||||||
if idx := strings.LastIndex(filename, "."); idx != -1 {
|
|
||||||
return filename[:idx]
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func copyToTmp(sourcePath string, backupFileName string) {
|
|
||||||
//Copy backup from storage to /tmp
|
|
||||||
err := utils.CopyFile(filepath.Join(sourcePath, backupFileName), filepath.Join(tmpPath, backupFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error copying file %s %s", backupFileName, err))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func moveToBackup(backupFileName string, destinationPath string) {
|
|
||||||
//Copy backup from tmp folder to storage destination
|
|
||||||
err := utils.CopyFile(filepath.Join(tmpPath, backupFileName), filepath.Join(destinationPath, backupFileName))
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error copying file %s %s", backupFileName, err))
|
|
||||||
|
|
||||||
}
|
|
||||||
//Delete backup file from tmp folder
|
|
||||||
err = utils.DeleteFile(filepath.Join(tmpPath, backupFileName))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error deleting file:", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
utils.Done("Database has been backed up and copied to %s", filepath.Join(destinationPath, backupFileName))
|
|
||||||
}
|
|
||||||
func deleteOldBackup(retentionDays int) {
|
|
||||||
utils.Info("Deleting old backups...")
|
|
||||||
storagePath = os.Getenv("STORAGE_PATH")
|
|
||||||
// Define the directory path
|
|
||||||
backupDir := storagePath + "/"
|
|
||||||
// Get current time
|
|
||||||
currentTime := time.Now()
|
|
||||||
// Delete file
|
|
||||||
deleteFile := func(filePath string) error {
|
|
||||||
err := os.Remove(filePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error: %s", err))
|
|
||||||
} else {
|
|
||||||
utils.Done("File %s has been deleted successfully", filePath)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk through the directory and delete files modified more than specified days ago
|
|
||||||
err := filepath.Walk(backupDir, func(filePath string, fileInfo os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Check if it's a regular file and if it was modified more than specified days ago
|
|
||||||
if fileInfo.Mode().IsRegular() {
|
|
||||||
timeDiff := currentTime.Sub(fileInfo.ModTime())
|
|
||||||
if timeDiff.Hours() > 24*float64(retentionDays) {
|
|
||||||
err := deleteFile(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error: %s", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-123
@@ -1,123 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func StartRestore(cmd *cobra.Command) {
|
|
||||||
|
|
||||||
//Set env
|
|
||||||
utils.SetEnv("STORAGE_PATH", storagePath)
|
|
||||||
utils.GetEnv(cmd, "dbname", "DB_NAME")
|
|
||||||
utils.GetEnv(cmd, "port", "DB_PORT")
|
|
||||||
|
|
||||||
//Get flag value and set env
|
|
||||||
s3Path := utils.GetEnv(cmd, "path", "AWS_S3_PATH")
|
|
||||||
remotePath := utils.GetEnv(cmd, "path", "SSH_REMOTE_PATH")
|
|
||||||
storage = utils.GetEnv(cmd, "storage", "STORAGE")
|
|
||||||
file = utils.GetEnv(cmd, "file", "FILE_NAME")
|
|
||||||
executionMode, _ = cmd.Flags().GetString("mode")
|
|
||||||
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
|
||||||
switch storage {
|
|
||||||
case "s3":
|
|
||||||
restoreFromS3(file, bucket, s3Path)
|
|
||||||
case "local":
|
|
||||||
utils.Info("Restore database from local")
|
|
||||||
copyToTmp(storagePath, file)
|
|
||||||
RestoreDatabase(file)
|
|
||||||
case "ssh":
|
|
||||||
restoreFromRemote(file, remotePath)
|
|
||||||
case "ftp":
|
|
||||||
utils.Fatal("Restore from FTP is not yet supported")
|
|
||||||
default:
|
|
||||||
utils.Info("Restore database from local")
|
|
||||||
RestoreDatabase(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func restoreFromS3(file, bucket, s3Path string) {
|
|
||||||
utils.Info("Restore database from s3")
|
|
||||||
err := utils.DownloadFile(tmpPath, file, bucket, s3Path)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error download file from s3 %s %s", file, err))
|
|
||||||
}
|
|
||||||
RestoreDatabase(file)
|
|
||||||
}
|
|
||||||
func restoreFromRemote(file, remotePath string) {
|
|
||||||
utils.Info("Restore database from remote server")
|
|
||||||
err := CopyFromRemote(file, remotePath)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error download file from remote server: ", filepath.Join(remotePath, file), err))
|
|
||||||
}
|
|
||||||
RestoreDatabase(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RestoreDatabase restore database
|
|
||||||
func RestoreDatabase(file string) {
|
|
||||||
dbHost = os.Getenv("DB_HOST")
|
|
||||||
dbPassword = os.Getenv("DB_PASSWORD")
|
|
||||||
dbUserName = os.Getenv("DB_USERNAME")
|
|
||||||
dbName = os.Getenv("DB_NAME")
|
|
||||||
dbPort = os.Getenv("DB_PORT")
|
|
||||||
gpgPassphrase := os.Getenv("GPG_PASSPHRASE")
|
|
||||||
if file == "" {
|
|
||||||
utils.Fatal("Error, file required")
|
|
||||||
}
|
|
||||||
|
|
||||||
err := utils.CheckEnvVars(dbHVars)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Please make sure all required environment variables for database are set")
|
|
||||||
utils.Fatal("Error checking environment variables: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
extension := filepath.Ext(fmt.Sprintf("%s/%s", tmpPath, file))
|
|
||||||
if extension == ".gpg" {
|
|
||||||
if gpgPassphrase == "" {
|
|
||||||
utils.Fatal("Error: GPG passphrase is required, your file seems to be a GPG file.\nYou need to provide GPG keys. GPG_PASSPHRASE environment variable is required.")
|
|
||||||
|
|
||||||
} else {
|
|
||||||
//Decrypt file
|
|
||||||
err := Decrypt(filepath.Join(tmpPath, file), gpgPassphrase)
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error decrypting file %s %v", file, err)
|
|
||||||
}
|
|
||||||
//Update file name
|
|
||||||
file = RemoveLastExtension(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.FileExists(fmt.Sprintf("%s/%s", tmpPath, file)) {
|
|
||||||
utils.TestDatabaseConnection()
|
|
||||||
|
|
||||||
extension := filepath.Ext(fmt.Sprintf("%s/%s", tmpPath, file))
|
|
||||||
// Restore from compressed file / .sql.gz
|
|
||||||
if extension == ".gz" {
|
|
||||||
str := "zcat " + fmt.Sprintf("%s/%s", tmpPath, file) + " | mysql -h " + os.Getenv("DB_HOST") + " -P " + os.Getenv("DB_PORT") + " -u " + os.Getenv("DB_USERNAME") + " --password=" + os.Getenv("DB_PASSWORD") + " " + os.Getenv("DB_NAME")
|
|
||||||
_, err := exec.Command("bash", "-c", str).Output()
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal("Error, in restoring the database %v", err)
|
|
||||||
}
|
|
||||||
utils.Done("Database has been restored")
|
|
||||||
|
|
||||||
} else if extension == ".sql" {
|
|
||||||
//Restore from sql file
|
|
||||||
str := "cat " + fmt.Sprintf("%s/%s", tmpPath, file) + " | mysql -h " + os.Getenv("DB_HOST") + " -P " + os.Getenv("DB_PORT") + " -u " + os.Getenv("DB_USERNAME") + " --password=" + os.Getenv("DB_PASSWORD") + " " + os.Getenv("DB_NAME")
|
|
||||||
_, err := exec.Command("bash", "-c", str).Output()
|
|
||||||
if err != nil {
|
|
||||||
utils.Fatal(fmt.Sprintf("Error in restoring the database %s", err))
|
|
||||||
}
|
|
||||||
utils.Done("Database has been restored")
|
|
||||||
} else {
|
|
||||||
utils.Fatal(fmt.Sprintf("Unknown file extension %s", extension))
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
utils.Fatal(fmt.Sprintf("File not found in %s", fmt.Sprintf("%s/%s", tmpPath, file)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/bramvdbogaerde/go-scp"
|
|
||||||
"github.com/bramvdbogaerde/go-scp/auth"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CopyToRemote(fileName, remotePath string) error {
|
|
||||||
sshUser := os.Getenv("SSH_USER")
|
|
||||||
sshPassword := os.Getenv("SSH_PASSWORD")
|
|
||||||
sshHostName := os.Getenv("SSH_HOST_NAME")
|
|
||||||
sshPort := os.Getenv("SSH_PORT")
|
|
||||||
sshIdentifyFile := os.Getenv("SSH_IDENTIFY_FILE")
|
|
||||||
|
|
||||||
err := utils.CheckEnvVars(sshHVars)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error checking environment variables: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig, _ := auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
|
||||||
if sshIdentifyFile != "" && utils.FileExists(sshIdentifyFile) {
|
|
||||||
clientConfig, _ = auth.PrivateKey(sshUser, sshIdentifyFile, ssh.InsecureIgnoreHostKey())
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if sshPassword == "" {
|
|
||||||
return errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty")
|
|
||||||
}
|
|
||||||
utils.Warn("Accessing the remote server using password, password is not recommended")
|
|
||||||
clientConfig, _ = auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
|
||||||
|
|
||||||
}
|
|
||||||
// Create a new SCP client
|
|
||||||
client := scp.NewClient(fmt.Sprintf("%s:%s", sshHostName, sshPort), &clientConfig)
|
|
||||||
|
|
||||||
// Connect to the remote server
|
|
||||||
err = client.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Couldn't establish a connection to the remote server")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open a file
|
|
||||||
file, _ := os.Open(filepath.Join(tmpPath, fileName))
|
|
||||||
|
|
||||||
// Close client connection after the file has been copied
|
|
||||||
defer client.Close()
|
|
||||||
// Close the file after it has been copied
|
|
||||||
defer file.Close()
|
|
||||||
// the context can be adjusted to provide time-outs or inherit from other contexts if this is embedded in a larger application.
|
|
||||||
err = client.CopyFromFile(context.Background(), *file, filepath.Join(remotePath, fileName), "0655")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error while copying file ")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyFromRemote(fileName, remotePath string) error {
|
|
||||||
sshUser := os.Getenv("SSH_USER")
|
|
||||||
sshPassword := os.Getenv("SSH_PASSWORD")
|
|
||||||
sshHostName := os.Getenv("SSH_HOST_NAME")
|
|
||||||
sshPort := os.Getenv("SSH_PORT")
|
|
||||||
sshIdentifyFile := os.Getenv("SSH_IDENTIFY_FILE")
|
|
||||||
|
|
||||||
err := utils.CheckEnvVars(sshHVars)
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Error checking environment variables\n: %s", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
clientConfig, _ := auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
|
||||||
if sshIdentifyFile != "" && utils.FileExists(sshIdentifyFile) {
|
|
||||||
clientConfig, _ = auth.PrivateKey(sshUser, sshIdentifyFile, ssh.InsecureIgnoreHostKey())
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if sshPassword == "" {
|
|
||||||
return errors.New("SSH_PASSWORD environment variable is required if SSH_IDENTIFY_FILE is empty\n")
|
|
||||||
}
|
|
||||||
utils.Warn("Accessing the remote server using password, password is not recommended")
|
|
||||||
clientConfig, _ = auth.PasswordKey(sshUser, sshPassword, ssh.InsecureIgnoreHostKey())
|
|
||||||
|
|
||||||
}
|
|
||||||
// Create a new SCP client
|
|
||||||
client := scp.NewClient(fmt.Sprintf("%s:%s", sshHostName, sshPort), &clientConfig)
|
|
||||||
|
|
||||||
// Connect to the remote server
|
|
||||||
err = client.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("Couldn't establish a connection to the remote server\n")
|
|
||||||
}
|
|
||||||
// Close client connection after the file has been copied
|
|
||||||
defer client.Close()
|
|
||||||
file, err := os.OpenFile(filepath.Join(tmpPath, fileName), os.O_RDWR|os.O_CREATE, 0777)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Couldn't open the output file")
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// the context can be adjusted to provide time-outs or inherit from other contexts if this is embedded in a larger application.
|
|
||||||
err = client.CopyFromRemote(context.Background(), file, filepath.Join(remotePath, fileName))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error while copying file ", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
// Package pkg /*
|
|
||||||
/*
|
|
||||||
Copyright © 2024 Jonas Kaninda
|
|
||||||
*/
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/jkaninda/mysql-bkup/utils"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CreateCrontabScript(disableCompression bool, storage string) {
|
|
||||||
//task := "/usr/local/bin/backup_cron.sh"
|
|
||||||
touchCmd := exec.Command("touch", backupCronFile)
|
|
||||||
if err := touchCmd.Run(); err != nil {
|
|
||||||
utils.Fatal("Error creating file %s: %v\n", backupCronFile, err)
|
|
||||||
}
|
|
||||||
var disableC = ""
|
|
||||||
if disableCompression {
|
|
||||||
disableC = "--disable-compression"
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptContent := fmt.Sprintf(`#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
bkup backup --dbname %s --port %s --storage %s %v
|
|
||||||
`, os.Getenv("DB_NAME"), os.Getenv("DB_PORT"), storage, disableC)
|
|
||||||
|
|
||||||
if err := utils.WriteToFile(backupCronFile, scriptContent); err != nil {
|
|
||||||
utils.Fatal("Error writing to %s: %v\n", backupCronFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
chmodCmd := exec.Command("chmod", "+x", "/usr/local/bin/backup_cron.sh")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
utils.Fatal("Error changing permissions of %s: %v\n", backupCronFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
lnCmd := exec.Command("ln", "-s", "/usr/local/bin/backup_cron.sh", "/usr/local/bin/backup_cron")
|
|
||||||
if err := lnCmd.Run(); err != nil {
|
|
||||||
utils.Fatal("Error creating symbolic link: %v\n", err)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
touchLogCmd := exec.Command("touch", cronLogFile)
|
|
||||||
if err := touchLogCmd.Run(); err != nil {
|
|
||||||
utils.Fatal("Error creating file %s: %v\n", cronLogFile, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cronJob := "/etc/cron.d/backup_cron"
|
|
||||||
touchCronCmd := exec.Command("touch", cronJob)
|
|
||||||
if err := touchCronCmd.Run(); err != nil {
|
|
||||||
utils.Fatal("Error creating file %s: %v\n", cronJob, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cronContent := fmt.Sprintf(`%s root exec /bin/bash -c ". /run/supervisord.env; /usr/local/bin/backup_cron.sh >> %s"
|
|
||||||
`, os.Getenv("SCHEDULE_PERIOD"), cronLogFile)
|
|
||||||
|
|
||||||
if err := utils.WriteToFile(cronJob, cronContent); err != nil {
|
|
||||||
utils.Fatal("Error writing to %s: %v\n", cronJob, err)
|
|
||||||
}
|
|
||||||
utils.ChangePermission("/etc/cron.d/backup_cron", 0644)
|
|
||||||
|
|
||||||
crontabCmd := exec.Command("crontab", "/etc/cron.d/backup_cron")
|
|
||||||
if err := crontabCmd.Run(); err != nil {
|
|
||||||
utils.Fatal("Error updating crontab: ", err)
|
|
||||||
}
|
|
||||||
utils.Info("Backup job created.")
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package pkg
|
|
||||||
|
|
||||||
const cronLogFile = "/var/log/mysql-bkup.log"
|
|
||||||
const tmpPath = "/tmp/backup"
|
|
||||||
const backupCronFile = "/usr/local/bin/backup_cron.sh"
|
|
||||||
const algorithm = "aes256"
|
|
||||||
const gpgExtension = "gpg"
|
|
||||||
|
|
||||||
var (
|
|
||||||
storage = "local"
|
|
||||||
file = ""
|
|
||||||
dbPassword = ""
|
|
||||||
dbUserName = ""
|
|
||||||
dbName = ""
|
|
||||||
dbHost = ""
|
|
||||||
dbPort = "3306"
|
|
||||||
executionMode = "default"
|
|
||||||
storagePath = "/backup"
|
|
||||||
disableCompression = false
|
|
||||||
encryption = false
|
|
||||||
)
|
|
||||||
|
|
||||||
// dbHVars Required environment variables for database
|
|
||||||
var dbHVars = []string{
|
|
||||||
"DB_HOST",
|
|
||||||
"DB_PASSWORD",
|
|
||||||
"DB_USERNAME",
|
|
||||||
"DB_NAME",
|
|
||||||
}
|
|
||||||
|
|
||||||
// sshHVars Required environment variables for SSH remote server storage
|
|
||||||
var sshHVars = []string{
|
|
||||||
"SSH_USER",
|
|
||||||
"SSH_REMOTE_PATH",
|
|
||||||
"SSH_HOST_NAME",
|
|
||||||
"SSH_PORT",
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
DB_USERNAME='db_username'
|
|
||||||
DB_PASSWORD='password'
|
|
||||||
DB_HOST='db_hostname'
|
|
||||||
DB_NAME='db_name'
|
|
||||||
BACKUP_DIR="$PWD/backup"
|
|
||||||
|
|
||||||
docker run --rm --name mysql-bkup -v $BACKUP_DIR:/backup/ -e "DB_HOST=$DB_HOST" -e "DB_USERNAME=$DB_USERNAME" -e "DB_PASSWORD=$DB_PASSWORD" jkaninda/mysql-bkup:latest backup -d $DB_NAME
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
const RestoreExample = "mysql-bkup restore --dbname database --file db_20231219_022941.sql.gz\n" +
|
|
||||||
"bkup restore --dbname database --storage s3 --path /custom-path --file db_20231219_022941.sql.gz"
|
|
||||||
const BackupExample = "mysql-bkup backup --dbname database --disable-compression\n" +
|
|
||||||
"mysql-bkup backup --dbname database --storage s3 --path /custom-path --disable-compression"
|
|
||||||
|
|
||||||
const MainExample = "mysql-bkup backup --dbname database --disable-compression\n" +
|
|
||||||
"mysql-bkup backup --dbname database --storage s3 --path /custom-path\n" +
|
|
||||||
"mysql-bkup restore --dbname database --file db_20231219_022941.sql.gz"
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var currentTime = time.Now().Format("2006/01/02 15:04:05")
|
|
||||||
|
|
||||||
func Info(msg string, args ...any) {
|
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
|
||||||
if len(args) == 0 {
|
|
||||||
fmt.Printf("%s INFO: %s\n", currentTime, msg)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s INFO: %s\n", currentTime, formattedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warn warning message
|
|
||||||
func Warn(msg string, args ...any) {
|
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
|
||||||
if len(args) == 0 {
|
|
||||||
fmt.Printf("%s WARN: %s\n", currentTime, msg)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s WARN: %s\n", currentTime, formattedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func Error(msg string, args ...any) {
|
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
|
||||||
if len(args) == 0 {
|
|
||||||
fmt.Printf("%s ERROR: %s\n", currentTime, msg)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s ERROR: %s\n", currentTime, formattedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func Done(msg string, args ...any) {
|
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
|
||||||
if len(args) == 0 {
|
|
||||||
fmt.Printf("%s INFO: %s\n", currentTime, msg)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s INFO: %s\n", currentTime, formattedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatal logs an error message and exits the program
|
|
||||||
func Fatal(msg string, args ...any) {
|
|
||||||
// Fatal logs an error message and exits the program.
|
|
||||||
formattedMessage := fmt.Sprintf(msg, args...)
|
|
||||||
if len(args) == 0 {
|
|
||||||
fmt.Printf("%s ERROR: %s\n", currentTime, msg)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("%s ERROR: %s\n", currentTime, formattedMessage)
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
-169
@@ -1,169 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateSession creates a new AWS session
|
|
||||||
func CreateSession() (*session.Session, error) {
|
|
||||||
// AwsVars Required environment variables for AWS S3 storage
|
|
||||||
var awsVars = []string{
|
|
||||||
"AWS_S3_ENDPOINT",
|
|
||||||
"AWS_S3_BUCKET_NAME",
|
|
||||||
"AWS_ACCESS_KEY",
|
|
||||||
"AWS_SECRET_KEY",
|
|
||||||
"AWS_REGION",
|
|
||||||
"AWS_REGION",
|
|
||||||
"AWS_REGION",
|
|
||||||
}
|
|
||||||
|
|
||||||
endPoint := GetEnvVariable("AWS_S3_ENDPOINT", "S3_ENDPOINT")
|
|
||||||
accessKey := GetEnvVariable("AWS_ACCESS_KEY", "ACCESS_KEY")
|
|
||||||
secretKey := GetEnvVariable("AWS_SECRET_KEY", "SECRET_KEY")
|
|
||||||
_ = GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
|
|
||||||
|
|
||||||
region := os.Getenv("AWS_REGION")
|
|
||||||
awsDisableSsl, err := strconv.ParseBool(os.Getenv("AWS_DISABLE_SSL"))
|
|
||||||
if err != nil {
|
|
||||||
Fatal("Unable to parse AWS_DISABLE_SSL env var: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = CheckEnvVars(awsVars)
|
|
||||||
if err != nil {
|
|
||||||
Fatal("Error checking environment variables\n: %s", err)
|
|
||||||
}
|
|
||||||
// S3 Config
|
|
||||||
s3Config := &aws.Config{
|
|
||||||
Credentials: credentials.NewStaticCredentials(accessKey, secretKey, ""),
|
|
||||||
Endpoint: aws.String(endPoint),
|
|
||||||
Region: aws.String(region),
|
|
||||||
DisableSSL: aws.Bool(awsDisableSsl),
|
|
||||||
S3ForcePathStyle: aws.Bool(true),
|
|
||||||
}
|
|
||||||
return session.NewSession(s3Config)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// UploadFileToS3 uploads a file to S3 with a given prefix
|
|
||||||
func UploadFileToS3(filePath, key, bucket, prefix string) error {
|
|
||||||
sess, err := CreateSession()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
svc := s3.New(sess)
|
|
||||||
|
|
||||||
file, err := os.Open(filepath.Join(filePath, key))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
fileInfo, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
objectKey := filepath.Join(prefix, key)
|
|
||||||
|
|
||||||
buffer := make([]byte, fileInfo.Size())
|
|
||||||
file.Read(buffer)
|
|
||||||
fileBytes := bytes.NewReader(buffer)
|
|
||||||
fileType := http.DetectContentType(buffer)
|
|
||||||
|
|
||||||
_, err = svc.PutObject(&s3.PutObjectInput{
|
|
||||||
Bucket: aws.String(bucket),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
Body: fileBytes,
|
|
||||||
ContentLength: aws.Int64(fileInfo.Size()),
|
|
||||||
ContentType: aws.String(fileType),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func DownloadFile(destinationPath, key, bucket, prefix string) error {
|
|
||||||
|
|
||||||
sess, err := CreateSession()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Info("Download backup from S3 storage...")
|
|
||||||
file, err := os.Create(filepath.Join(destinationPath, key))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to create file", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
objectKey := filepath.Join(prefix, key)
|
|
||||||
|
|
||||||
downloader := s3manager.NewDownloader(sess)
|
|
||||||
numBytes, err := downloader.Download(file,
|
|
||||||
&s3.GetObjectInput{
|
|
||||||
Bucket: aws.String(bucket),
|
|
||||||
Key: aws.String(objectKey),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Failed to download file", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
Info(fmt.Sprintf("Backup downloaded: ", file.Name(), " bytes size ", numBytes))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func DeleteOldBackup(bucket, prefix string, retention int) error {
|
|
||||||
sess, err := CreateSession()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
svc := s3.New(sess)
|
|
||||||
|
|
||||||
// Get the current time and the time threshold for 7 days ago
|
|
||||||
now := time.Now()
|
|
||||||
backupRetentionDays := now.AddDate(0, 0, -retention)
|
|
||||||
|
|
||||||
// List objects in the bucket
|
|
||||||
listObjectsInput := &s3.ListObjectsV2Input{
|
|
||||||
Bucket: aws.String(bucket),
|
|
||||||
Prefix: aws.String(prefix),
|
|
||||||
}
|
|
||||||
err = svc.ListObjectsV2Pages(listObjectsInput, func(page *s3.ListObjectsV2Output, lastPage bool) bool {
|
|
||||||
for _, object := range page.Contents {
|
|
||||||
if object.LastModified.Before(backupRetentionDays) {
|
|
||||||
// Object is older than retention days, delete it
|
|
||||||
_, err := svc.DeleteObject(&s3.DeleteObjectInput{
|
|
||||||
Bucket: aws.String(bucket),
|
|
||||||
Key: object.Key,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to delete object %s: %v", *object.Key, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Deleted object %s\n", *object.Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !lastPage
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Failed to list objects: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Finished deleting old files.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
-184
@@ -1,184 +0,0 @@
|
|||||||
package utils
|
|
||||||
|
|
||||||
/*****
|
|
||||||
* MySQL Backup & Restore
|
|
||||||
* @author Jonas Kaninda
|
|
||||||
* @license MIT License <https://opensource.org/licenses/MIT>
|
|
||||||
* @link https://github.com/jkaninda/mysql-bkup
|
|
||||||
**/
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FileExists(filename string) bool {
|
|
||||||
info, err := os.Stat(filename)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return !info.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteToFile(filePath, content string) error {
|
|
||||||
file, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
_, err = file.WriteString(content)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func DeleteFile(filePath string) error {
|
|
||||||
err := os.Remove(filePath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to delete file: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func CopyFile(src, dst string) error {
|
|
||||||
// Open the source file for reading
|
|
||||||
sourceFile, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open source file: %v", err)
|
|
||||||
}
|
|
||||||
defer sourceFile.Close()
|
|
||||||
|
|
||||||
// Create the destination file
|
|
||||||
destinationFile, err := os.Create(dst)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create destination file: %v", err)
|
|
||||||
}
|
|
||||||
defer destinationFile.Close()
|
|
||||||
|
|
||||||
// Copy the content from source to destination
|
|
||||||
_, err = io.Copy(destinationFile, sourceFile)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to copy file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush the buffer to ensure all data is written
|
|
||||||
err = destinationFile.Sync()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to sync destination file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func ChangePermission(filePath string, mod int) {
|
|
||||||
if err := os.Chmod(filePath, fs.FileMode(mod)); err != nil {
|
|
||||||
Fatal("Error changing permissions of %s: %v\n", filePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
func IsDirEmpty(name string) (bool, error) {
|
|
||||||
f, err := os.Open(name)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
_, err = f.Readdirnames(1)
|
|
||||||
if err == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDatabaseConnection tests the database connection
|
|
||||||
func TestDatabaseConnection() {
|
|
||||||
dbHost := os.Getenv("DB_HOST")
|
|
||||||
dbPassword := os.Getenv("DB_PASSWORD")
|
|
||||||
dbUserName := os.Getenv("DB_USERNAME")
|
|
||||||
dbName := os.Getenv("DB_NAME")
|
|
||||||
dbPort := os.Getenv("DB_PORT")
|
|
||||||
|
|
||||||
if os.Getenv("DB_HOST") == "" || os.Getenv("DB_NAME") == "" || os.Getenv("DB_USERNAME") == "" || os.Getenv("DB_PASSWORD") == "" {
|
|
||||||
Fatal("Please make sure all required database environment variables are set")
|
|
||||||
} else {
|
|
||||||
Info("Connecting to database ...")
|
|
||||||
|
|
||||||
cmd := exec.Command("mysql", "-h", dbHost, "-P", dbPort, "-u", dbUserName, "--password="+dbPassword, dbName, "-e", "quit")
|
|
||||||
|
|
||||||
// Capture the output
|
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &out
|
|
||||||
err := cmd.Run()
|
|
||||||
if err != nil {
|
|
||||||
Error("Error testing database connection: %v\nOutput: %s", err, out.String())
|
|
||||||
os.Exit(1)
|
|
||||||
|
|
||||||
}
|
|
||||||
Info("Successfully connected to database")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func GetEnv(cmd *cobra.Command, flagName, envName string) string {
|
|
||||||
value, _ := cmd.Flags().GetString(flagName)
|
|
||||||
if value != "" {
|
|
||||||
err := os.Setenv(envName, value)
|
|
||||||
if err != nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.Getenv(envName)
|
|
||||||
}
|
|
||||||
func FlagGetString(cmd *cobra.Command, flagName string) string {
|
|
||||||
value, _ := cmd.Flags().GetString(flagName)
|
|
||||||
if value != "" {
|
|
||||||
return value
|
|
||||||
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
func FlagGetBool(cmd *cobra.Command, flagName string) bool {
|
|
||||||
value, _ := cmd.Flags().GetBool(flagName)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetEnv(key, value string) {
|
|
||||||
|
|
||||||
err := os.Setenv(key, value)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
func GetEnvVariable(envName, oldEnvName string) string {
|
|
||||||
value := os.Getenv(envName)
|
|
||||||
if value == "" {
|
|
||||||
value = os.Getenv(oldEnvName)
|
|
||||||
if value != "" {
|
|
||||||
err := os.Setenv(envName, value)
|
|
||||||
if err != nil {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
Warn("%s is deprecated, please use %s instead!", oldEnvName, envName)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
func ShowHistory() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckEnvVars checks if all the specified environment variables are set
|
|
||||||
func CheckEnvVars(vars []string) error {
|
|
||||||
missingVars := []string{}
|
|
||||||
|
|
||||||
for _, v := range vars {
|
|
||||||
if os.Getenv(v) == "" {
|
|
||||||
missingVars = append(missingVars, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(missingVars) > 0 {
|
|
||||||
return fmt.Errorf("missing environment variables: %v", missingVars)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user