Compare commits

...
241 Commits
Author SHA1 Message Date
jkaninda dea2eedc35 Merge pull request #220 from jkaninda/v2
Deploy Documenation site to GitHub Pages / build (push) Canceled after 0s
Deploy Documenation site to GitHub Pages / deploy (push) Canceled after 0s
Lint / golangci-lint (push) Canceled after 0s
Tests / Vet and unit tests (push) Canceled after 0s
Tests / Integration tests (MySQL 8.4 / 8.0, MariaDB 11.8, MinIO) (push) Canceled after 0s
feat: mysql-bkup v2 on bkupkit
2026-10-28 12:22:08 +01:00
jkaninda 28de0d7b52 feat: mysql-bkup v2 on bkupkit
MysQL backup and restore built on the shared bkupkit core:
streaming end to end, with no dump held in memory or written to
local disk.
2026-10-28 11:18:56 +01:00
jkaninda ccb3fcdf26 upgrade go version (#214)
Lint / Run on Ubuntu (push) Has been cancelled
Tests / test (push) Has been cancelled
2026-02-17 21:59:02 +01:00
jkaninda fff1e1f9a9 Merge pull request #206 from jkaninda/dependabot/go_modules/github.com/spf13/cobra-1.10.2
chore(deps): bump github.com/spf13/cobra from 1.10.1 to 1.10.2
2026-02-17 21:44:01 +01:00
jkaninda e7a50743ed Merge branch 'main' into dependabot/go_modules/github.com/spf13/cobra-1.10.2 2026-02-17 21:43:49 +01:00
jkaninda 47220bf005 Merge pull request #209 from jkaninda/dependabot/go_modules/github.com/jkaninda/go-utils-0.1.4
chore(deps): bump github.com/jkaninda/go-utils from 0.1.3 to 0.1.4
2026-02-17 21:43:38 +01:00
jkaninda 0ae9cffee0 Merge branch 'main' into dependabot/go_modules/github.com/jkaninda/go-utils-0.1.4 2026-02-17 21:43:26 +01:00
jkaninda 0b08e0498a Merge pull request #211 from jkaninda/dependabot/docker/golang-1.25.6
chore(deps): bump golang from 1.25.1 to 1.25.6
2026-02-17 21:43:12 +01:00
jkaninda aef2395272 Merge pull request #212 from jkaninda/dependabot/docker/alpine-3.23.3
chore(deps): bump alpine from 3.22.1 to 3.23.3
2026-02-17 21:42:54 +01:00
dependabot[bot] 69f4795adb chore(deps): bump alpine from 3.22.1 to 3.23.3
Bumps alpine from 3.22.1 to 3.23.3.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: 3.23.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 09:32:52 +00:00
dependabot[bot] f5df2081f1 chore(deps): bump golang from 1.25.1 to 1.25.6
Bumps golang from 1.25.1 to 1.25.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-26 09:32:00 +00:00
dependabot[bot] a1c991b363 chore(deps): bump github.com/jkaninda/go-utils from 0.1.3 to 0.1.4
Bumps [github.com/jkaninda/go-utils](https://github.com/jkaninda/go-utils) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/jkaninda/go-utils/releases)
- [Commits](https://github.com/jkaninda/go-utils/compare/v0.1.3...v0.1.4)

---
updated-dependencies:
- dependency-name: github.com/jkaninda/go-utils
  dependency-version: 0.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 09:11:26 +00:00
dependabot[bot] 416e094065 chore(deps): bump github.com/spf13/cobra from 1.10.1 to 1.10.2
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.10.1 to 1.10.2.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.10.1...v1.10.2)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-08 09:15:03 +00:00
jkaninda 8ac6ffc394 Merge pull request #197 from jkaninda/dependabot/go_modules/github.com/jkaninda/go-utils-0.1.3
Tests / test (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
chore(deps): bump github.com/jkaninda/go-utils from 0.1.1 to 0.1.3
2025-09-10 05:56:13 +02:00
jkaninda 59296c0c17 Merge branch 'main' into dependabot/go_modules/github.com/jkaninda/go-utils-0.1.3 2025-09-10 05:55:13 +02:00
jkaninda 19b64bbad1 Merge pull request #198 from jkaninda/dependabot/docker/golang-1.25.1
chore(deps): bump golang from 1.24.6 to 1.25.1
2025-09-10 05:54:28 +02:00
jkaninda 640e11b924 Merge pull request #199 from jkaninda/dependabot/go_modules/github.com/spf13/cobra-1.10.1
chore(deps): bump github.com/spf13/cobra from 1.9.1 to 1.10.1
2025-09-10 05:54:15 +02:00
dependabot[bot] a746366348 chore(deps): bump github.com/spf13/cobra from 1.9.1 to 1.10.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.9.1 to 1.10.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:25:00 +00:00
dependabot[bot] 64d57a077e chore(deps): bump golang from 1.24.6 to 1.25.1
Bumps golang from 1.24.6 to 1.25.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.25.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 09:21:49 +00:00
dependabot[bot] 995db83b7c chore(deps): bump github.com/jkaninda/go-utils from 0.1.1 to 0.1.3
Bumps [github.com/jkaninda/go-utils](https://github.com/jkaninda/go-utils) from 0.1.1 to 0.1.3.
- [Release notes](https://github.com/jkaninda/go-utils/releases)
- [Commits](https://github.com/jkaninda/go-utils/compare/v0.1.1...v0.1.3)

---
updated-dependencies:
- dependency-name: github.com/jkaninda/go-utils
  dependency-version: 0.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-25 17:37:38 +00:00
jkaninda 5d5cbea60b Merge pull request #194 from jkaninda/dependabot/docker/alpine-3.22.1
Lint / Run on Ubuntu (push) Has been cancelled
Tests / test (push) Has been cancelled
chore(deps): bump alpine from 3.22.0 to 3.22.1
2025-08-13 04:45:07 +02:00
dependabot[bot] 2cb5603b88 chore(deps): bump alpine from 3.22.0 to 3.22.1
Lint / Run on Ubuntu (push) Has been cancelled
Bumps alpine from 3.22.0 to 3.22.1.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: 3.22.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 18:28:38 +00:00
jkaninda 548de725dd Merge pull request #195 from jkaninda/dependabot/docker/golang-1.24.6
Lint / Run on Ubuntu (push) Has been cancelled
Tests / test (push) Has been cancelled
chore(deps): bump golang from 1.24.5 to 1.24.6
2025-08-11 20:24:27 +02:00
dependabot[bot] 37b2867974 chore(deps): bump golang from 1.24.5 to 1.24.6
Bumps golang from 1.24.5 to 1.24.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-11 14:49:26 +00:00
jkaninda d6c01c9a4a Merge pull request #193 from jkaninda/dependabot/docker/golang-1.24.5
Tests / test (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
chore(deps): bump golang from 1.24.4 to 1.24.5
2025-07-19 08:00:22 +02:00
dependabot[bot] bd92cc6844 chore(deps): bump golang from 1.24.4 to 1.24.5
Bumps golang from 1.24.4 to 1.24.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-14 12:14:33 +00:00
jkaninda 3006abac3e Merge pull request #192 from jkaninda/dependabot/docker/golang-1.24.4
Tests / test (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
chore(deps): bump golang from 1.24.3 to 1.24.4
2025-06-09 19:53:25 +02:00
dependabot[bot] 3a16f6929c chore(deps): bump golang from 1.24.3 to 1.24.4
Bumps golang from 1.24.3 to 1.24.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 10:17:20 +00:00
jkaninda 2f3ed7d0d8 Merge pull request #191 from jkaninda/dependabot/docker/alpine-3.22.0
chore(deps): bump alpine from 3.21.3 to 3.22.0
2025-06-06 20:01:27 +02:00
dependabot[bot] a92bba05e4 chore(deps): bump alpine from 3.21.3 to 3.22.0
Bumps alpine from 3.21.3 to 3.22.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: 3.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-02 11:09:07 +00:00
jkaninda 7b565d54bd Merge pull request #190 from jkaninda/dependabot/docker/golang-1.24.3
Tests / test (push) Failing after 1m14s
Lint / Run on Ubuntu (push) Successful in 18m52s
chore(deps): bump golang from 1.24.2 to 1.24.3
2025-05-14 23:03:58 +02:00
dependabot[bot] d10321dac6 chore(deps): bump golang from 1.24.2 to 1.24.3
Bumps golang from 1.24.2 to 1.24.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 09:54:31 +00:00
jkaninda d151c50caa Merge pull request #189 from jkaninda/nightly
Tests / test (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
Nightly
2025-05-11 20:10:05 +02:00
jkaninda 07ecec57b3 Add why use MySQL-BKUP 2025-05-11 20:09:19 +02:00
jkaninda bc4aab6ed0 Add Docker labels 2025-05-11 20:05:16 +02:00
jkaninda 12c17c18d6 doc: update key features (#188)
Tests / test (push) Has been cancelled
Deploy Documenation site to GitHub Pages / build (push) Has been cancelled
Lint / Run on Ubuntu (push) Has been cancelled
Deploy Documenation site to GitHub Pages / deploy (push) Has been cancelled
2025-05-10 17:47:12 +02:00
jkaninda 80cd70e153 Merge pull request #187 from jkaninda/dependabot/docker/golang-1.24.2
Lint / Run on Ubuntu (push) Successful in 18m40s
Tests / test (push) Failing after 25s
chore(deps): bump golang from 1.24.1 to 1.24.2
2025-04-08 19:29:23 +02:00
dependabot[bot] 80abdb4299 chore(deps): bump golang from 1.24.1 to 1.24.2
Bumps golang from 1.24.1 to 1.24.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-07 10:37:49 +00:00
jkaninda 72b21f00e6 Merge pull request #186 from jkaninda/nightly
Lint / Run on Ubuntu (push) Successful in 18m44s
Tests / test (push) Failing after 18s
Nightly
2025-03-17 09:09:09 +01:00
jkaninda 3439c74257 Merge branch 'main' of github.com:jkaninda/mysql-bkup into nightly 2025-03-17 09:05:24 +01:00
jkaninda 4110ff4e64 Merge branch 'docs' into nightly 2025-03-17 09:05:16 +01:00
jkaninda cb299a35bf enh: enhancement of logging 2025-03-17 09:05:04 +01:00
jkaninda ba7c096bf3 Merge pull request #185 from jkaninda/docs
Deploy Documenation site to GitHub Pages / build (push) Failing after 9m27s
Deploy Documenation site to GitHub Pages / deploy (push) Has been skipped
Lint / Run on Ubuntu (push) Successful in 18m44s
Tests / test (push) Failing after 20s
doc: update deployment examples
2025-03-16 11:50:32 +01:00
jkaninda 6ac8dcef9e doc: update deployment examples
Lint / Run on Ubuntu (push) Successful in 18m39s
2025-03-16 11:49:36 +01:00
jkaninda 6bcc5d6bd4 Merge pull request #184 from jkaninda/docs
Deploy Documenation site to GitHub Pages / build (push) Failing after 9m27s
Deploy Documenation site to GitHub Pages / deploy (push) Has been skipped
Lint / Run on Ubuntu (push) Successful in 18m36s
Tests / test (push) Failing after 18s
doc: update configuration deployment
2025-03-16 05:55:14 +01:00
jkaninda fd35fabf97 doc: update configuration deployment 2025-03-16 05:45:12 +01:00
jkaninda 23cce10e8c Merge branch 'main' of github.com:jkaninda/mysql-bkup into nightly 2025-03-16 05:37:34 +01:00
jkaninda e666466d27 fix: database name not set error when using flag -d (#183)
Lint / Run on Ubuntu (push) Successful in 18m39s
Tests / test (push) Failing after 13s
* fix: database name not set error when using flag -d
2025-03-14 14:38:06 +01:00
jkaninda 36bca254a9 ci: Set -d flag for database name
Build / docker (push) Failing after 8s
Tests / test (push) Failing after 1m0s
Lint / Run on Ubuntu (push) Successful in 18m37s
2025-03-14 14:34:25 +01:00
jkaninda ad18d42145 fix: database name not set error when using flag -d 2025-03-14 14:32:38 +01:00
jkaninda d9d44c2798 Merge pull request #182 from jkaninda/nightly
Deploy Documenation site to GitHub Pages / build (push) Failing after 9m29s
Deploy Documenation site to GitHub Pages / deploy (push) Has been skipped
Lint / Run on Ubuntu (push) Successful in 18m40s
Tests / test (push) Failing after 2m19s
Nightly
2025-03-14 09:59:33 +01:00
jkaninda 300a592508 Merge branch 'main' of github.com:jkaninda/mysql-bkup into nightly
Build / docker (push) Failing after 9s
Lint / Run on Ubuntu (push) Successful in 18m40s
Tests / test (push) Failing after 19s
2025-03-14 09:58:47 +01:00
jkaninda be82e841e7 ci: set docker tests on main 2025-03-14 09:58:41 +01:00
jkaninda a73a365ebf ci: set docker tests on main 2025-03-14 09:57:59 +01:00
jkaninda 75e965c0c5 Merge pull request #181 from jkaninda/nightly
doc: update reference
2025-03-14 09:55:02 +01:00
jkaninda fc60ddb308 doc: update reference 2025-03-14 09:53:38 +01:00
jkaninda 573ef15ef3 Merge pull request #180 from jkaninda/nightly
ci: update  Github pages action
2025-03-14 09:50:49 +01:00
jkaninda b1776d3689 ci: update Github pages action 2025-03-14 09:50:13 +01:00
jkaninda 376d47f738 Merge pull request #178 from jkaninda/nightly
feat: add backup all databases separately
2025-03-14 09:43:43 +01:00
jkaninda eb6268f8ec ci: add Docker tests (#179) 2025-03-14 09:41:37 +01:00
jkaninda 731e2d789d ci: add go lint 2025-03-14 05:24:46 +01:00
jkaninda 6300a8f2dd feat: add backup all databases 2025-03-14 05:20:54 +01:00
jkaninda cd827a9277 chore: comment code
Build / docker (push) Failing after 16s
2025-03-13 14:44:22 +01:00
jkaninda 71cf3fae85 chore: improve log message 2025-03-13 14:26:32 +01:00
jkaninda 528282bbd4 feat: add backup all databases separately 2025-03-13 07:48:28 +01:00
jkaninda 002c93a796 Merge pull request #176 from jkaninda/dependabot/docker/golang-1.24.1
chore(deps): bump golang from 1.24.0 to 1.24.1
2025-03-12 16:29:14 +01:00
jkaninda b6192f4c42 feat: add backup all databases
Build / docker (push) Failing after 14s
2025-03-12 16:04:26 +01:00
jkaninda d5061453b0 feat: add backup all databases 2025-03-12 15:50:30 +01:00
jkaninda 0bc7497512 fix: warning message when using MYSQL_PASSWORD env 2025-03-12 14:13:21 +01:00
jkaninda 489dfdf842 fix: backup error output 2025-03-12 13:27:31 +01:00
dependabot[bot] 907e70d552 chore(deps): bump golang from 1.24.0 to 1.24.1
Bumps golang from 1.24.0 to 1.24.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 09:25:40 +00:00
jkaninda 696477fe5c Merge pull request #175 from jkaninda/dependabot/go_modules/github.com/jkaninda/go-utils-0.1.1
chore(deps): bump github.com/jkaninda/go-utils from 0.0.0-20250122060806-26119182077a to 0.1.1
2025-02-27 12:33:17 +01:00
dependabot[bot] 56a8b51660 chore(deps): bump github.com/jkaninda/go-utils
Bumps [github.com/jkaninda/go-utils](https://github.com/jkaninda/go-utils) from 0.0.0-20250122060806-26119182077a to 0.1.1.
- [Release notes](https://github.com/jkaninda/go-utils/releases)
- [Commits](https://github.com/jkaninda/go-utils/commits/v0.1.1)

---
updated-dependencies:
- dependency-name: github.com/jkaninda/go-utils
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 10:27:41 +00:00
jkaninda c76a00139c Merge pull request #172 from jkaninda/dependabot/go_modules/github.com/spf13/cobra-1.9.1
chore(deps): bump github.com/spf13/cobra from 1.8.1 to 1.9.1
2025-02-21 11:38:48 +01:00
jkaninda 0f43871765 Merge pull request #173 from jkaninda/dependabot/docker/golang-1.24.0
chore(deps): bump golang from 1.23.6 to 1.24.0
2025-02-21 11:38:37 +01:00
jkaninda 9ba6abe3f4 Merge pull request #174 from jkaninda/dependabot/docker/alpine-3.21.3
chore(deps): bump alpine from 3.21.2 to 3.21.3
2025-02-21 11:38:27 +01:00
dependabot[bot] 764583d88f chore(deps): bump alpine from 3.21.2 to 3.21.3
Bumps alpine from 3.21.2 to 3.21.3.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 10:11:17 +00:00
dependabot[bot] dbf4dc596a chore(deps): bump golang from 1.23.6 to 1.24.0
Bumps golang from 1.23.6 to 1.24.0.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 10:11:10 +00:00
dependabot[bot] 06c89a9b78 chore(deps): bump github.com/spf13/cobra from 1.8.1 to 1.9.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.1 to 1.9.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.1...v1.9.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-17 10:01:09 +00:00
jkaninda ec8bdd806c Merge pull request #171 from jkaninda/dependabot/docker/golang-1.23.6
chore(deps): bump golang from 1.23.5 to 1.23.6
2025-02-10 20:15:31 +01:00
dependabot[bot] 828b11c6dd chore(deps): bump golang from 1.23.5 to 1.23.6
Bumps golang from 1.23.5 to 1.23.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 09:34:49 +00:00
jkaninda 1d01e13909 Merge pull request #170 from jkaninda/nightly
chore: update helper func to check env with prefix or suffix for multi backups
2025-02-05 07:44:57 +01:00
jkaninda bd65db2418 chore: update helper func to check env with prefix or suffix for multi backups
Build / docker (push) Failing after 14m58s
2025-02-05 07:39:52 +01:00
jkaninda 75b809511e fix go lint
Build / docker (push) Failing after 8s
2025-01-26 13:54:41 +01:00
jkaninda fc028a2c55 feat: add multiple backup rescued mode for scheduled mode 2025-01-26 13:43:39 +01:00
jkaninda 7fa0c6a118 Merge pull request #169 from jkaninda/nightly
Deploy Documenation site to GitHub Pages / build (push) Failing after 9m23s
Deploy Documenation site to GitHub Pages / deploy (push) Has been skipped
docs: add quick restore
2025-01-26 12:12:53 +01:00
jkaninda 661702a97e docs: add quick restore 2025-01-26 12:11:29 +01:00
jkaninda dd5f33f17d Merge pull request #168 from jkaninda/nightly
Deploy Documenation site to GitHub Pages / build (push) Failing after 9m28s
Deploy Documenation site to GitHub Pages / deploy (push) Has been skipped
Nightly
2025-01-25 09:36:19 +01:00
jkaninda b7cdfebd9c chore: notification remove MAIL_USERNAME and MAIL_PASSWORD from required env
Build / docker (push) Failing after 13s
2025-01-25 09:19:23 +01:00
jkaninda 4b93becdf2 feat: add Set default values from environment variables if not provided for multiple backup 2025-01-25 09:12:28 +01:00
jkaninda 748cccec58 Merge pull request #167 from jkaninda/nightly
feat: add backup duration
2025-01-22 07:23:29 +01:00
jkaninda 3e8bfabc44 feat: add backup duration
Build / docker (push) Failing after 12s
2025-01-22 07:22:56 +01:00
jkaninda 777b59fd7c Merge pull request #166 from jkaninda/dependabot/docker/golang-1.23.5
chore(deps): bump golang from 1.23.4 to 1.23.5
2025-01-21 02:53:48 +01:00
dependabot[bot] 2b25f39c0a chore(deps): bump golang from 1.23.4 to 1.23.5
Bumps golang from 1.23.4 to 1.23.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 10:09:46 +00:00
jkaninda e5ba397bb4 Merge pull request #164 from jkaninda/nightly
Deploy Documenation site to GitHub Pages / deploy (push) Has been skipped
Deploy Documenation site to GitHub Pages / build (push) Failing after 9m27s
doc: reviewed docs
2025-01-13 15:34:50 +01:00
jkaninda 3a1bfc512d doc: reviewed docs
Build / docker (push) Failing after 9s
2025-01-13 15:34:02 +01:00
jkaninda b7b09ad6fd Merge pull request #163 from jkaninda/nightly
Nightly
2025-01-13 15:06:27 +01:00
jkaninda 1206140a67 doc: reviewed docs 2025-01-13 15:05:50 +01:00
jkaninda 24573a96ad doc: reviewed docs 2025-01-13 15:04:29 +01:00
jkaninda fff0b55722 Merge pull request #162 from jkaninda/nightly
feat: add backup flags for configuration and cron expression
2025-01-13 14:57:00 +01:00
jkaninda 68322e6b9f doc: reviewed docs 2025-01-13 14:56:08 +01:00
jkaninda 0f28772659 doc: reviewed docs 2025-01-13 14:40:46 +01:00
jkaninda b95ccf3905 feat: add backup flags for configuration and cron expression 2025-01-13 14:23:27 +01:00
jkaninda a06872834f Merge pull request #161 from jkaninda/dependabot/docker/alpine-3.21.2
chore(deps): bump alpine from 3.21.0 to 3.21.2
2025-01-13 10:53:41 +01:00
dependabot[bot] 393168c6c5 chore(deps): bump alpine from 3.21.0 to 3.21.2
Bumps alpine from 3.21.0 to 3.21.2.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 09:52:30 +00:00
jkaninda 5b9ec8a224 Merge pull request #160 from jkaninda/nightly
fix: the configuration file path is not being detected when it is enc…
2025-01-12 09:36:29 +01:00
jkaninda 2c3f2f4a46 fix: the configuration file path is not being detected when it is enclosed in quotes 2025-01-12 07:58:32 +01:00
jkaninda 0df14f37b4 Merge pull request #159 from jkaninda/refactor
chore: add convert bytes to a human-readable string with the appropri…
2024-12-12 13:29:22 +01:00
jkaninda 1b60ca6fd2 chore: add convert bytes to a human-readable string with the appropriate unit (bytes, MiB, or GiB) 2024-12-12 13:28:09 +01:00
jkaninda d880f40108 Merge pull request #158 from jkaninda/dependabot/docker/golang-1.23.4
chore(deps): bump golang from 1.23.3 to 1.23.4
2024-12-10 10:20:27 +01:00
dependabot[bot] c845b36797 chore(deps): bump golang from 1.23.3 to 1.23.4
Bumps golang from 1.23.3 to 1.23.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 09:53:21 +00:00
jkaninda 63d615f838 Merge pull request #157 from jkaninda/refactor
docs: add azure configuration reference
2024-12-08 13:36:26 +01:00
jkaninda 6f31d35df2 docs: add azure configuration reference 2024-12-08 13:35:49 +01:00
jkaninda f36d01cc96 Merge pull request #156 from jkaninda/refactor
Fix grammar issues in azure.go
2024-12-08 00:01:13 +01:00
jkaninda 07b7f54a75 Fix grammar issues in azure.go 2024-12-08 00:00:24 +01:00
jkaninda 7ff9a32f08 Merge pull request #155 from jkaninda/develop
chore: update notification template
2024-12-07 20:29:13 +01:00
jkaninda 95a81cb6b7 fix: SSH storage key identitify file 2024-12-07 20:14:30 +01:00
jkaninda 057d5277b0 fix: deprecation warning message, replace mysql by mariadb command 2024-12-07 17:54:44 +01:00
jkaninda 8e58d7a4c3 chore: update notification template 2024-12-07 17:36:05 +01:00
jkaninda 4bd7d9fa72 Merge pull request #154 from jkaninda/refactor
chore: update .env.example
2024-12-07 03:24:35 +01:00
Jonas Kaninda 156f22f1e5 chore: update .env.example 2024-12-07 03:24:06 +01:00
jkaninda fd444293b4 Merge pull request #153 from jkaninda/refactor
fix: S3 remote path when backing up multiple databases
2024-12-07 02:34:42 +01:00
Jonas Kaninda 1940ceba9a fix: S3 remote path when backing up multiple databases 2024-12-07 02:25:22 +01:00
Jonas Kaninda 07d580a8a9 refactoring of code 2024-12-07 02:23:38 +01:00
jkaninda 9a261b22ec Merge pull request #152 from jkaninda/refactor
docs: update features
2024-12-06 22:09:12 +01:00
Jonas Kaninda e7a58f0569 docs: update features 2024-12-06 22:08:51 +01:00
jkaninda 1b529725d7 Merge pull request #151 from jkaninda/refactor
fix: fatal logger notification
2024-12-06 21:03:47 +01:00
Jonas Kaninda d8c73560b8 fix: fatal logger notification 2024-12-06 21:00:26 +01:00
Jonas Kaninda d5a0adc981 refactoring of code 2024-12-06 20:53:46 +01:00
jkaninda 6df3bae9e2 Merge pull request #150 from jkaninda/feature/azure-blob
chore: update base image tag version
2024-12-06 20:23:46 +01:00
Jonas Kaninda f7d624fd15 chore: update base image tag version 2024-12-06 20:23:08 +01:00
jkaninda 1e9e1ed951 Merge pull request #149 from jkaninda/feature/azure-blob
chore: update app package
2024-12-06 20:17:57 +01:00
Jonas Kaninda 917ba8947f chore: update app package 2024-12-06 20:16:56 +01:00
jkaninda 94a1dcdff7 Merge pull request #148 from jkaninda/feature/azure-blob
Feature/azure blob
2024-12-06 18:34:46 +01:00
Jonas Kaninda f70e549b16 docs: update Azure storage 2024-12-06 18:33:54 +01:00
Jonas Kaninda 607478fcc6 docs: update Azure storage 2024-12-06 18:33:17 +01:00
jkaninda 2862e504f5 Merge pull request #147 from jkaninda/feature/azure-blob
feat: add Azure Blob storage
2024-12-06 18:31:08 +01:00
Jonas Kaninda 29420ee13e feat: add Azure Blob storage 2024-12-06 18:30:38 +01:00
jkaninda f53272ccf0 Merge pull request #146 from jkaninda/feature/azure-blob
feat: add Azure Blob storage
2024-12-06 18:29:34 +01:00
Jonas Kaninda c360441445 feat: add Azure Blob storage 2024-12-06 18:27:25 +01:00
Jonas Kaninda f6916231f7 docs: update core features 2024-12-06 14:23:06 +01:00
Jonas Kaninda afd4afc83b refactor: refactoring of code 2024-12-06 14:21:55 +01:00
Jonas Kaninda 9016a9ec7a Add LICENSE 2024-12-06 03:25:38 +01:00
jkaninda 4ecd96e75c Merge pull request #145 from jkaninda/dependabot/docker/golang-1.23.3
chore(deps): bump golang from 1.23.2 to 1.23.3
2024-11-11 11:04:56 +01:00
dependabot[bot] 8a88e4a727 chore(deps): bump golang from 1.23.2 to 1.23.3
Bumps golang from 1.23.2 to 1.23.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 09:58:46 +00:00
jkaninda 62f86adea9 Merge pull request #144 from jkaninda/refactor
refactor: Restructure project files for better organization, readabil…
2024-11-04 09:02:57 +01:00
jkaninda eb414d818c refactor: Restructure project files for better organization, readability, and maintainability 2024-11-04 09:02:41 +01:00
jkaninda 6721cc430d Merge pull request #143 from jkaninda/refactor
refactor: update logger, fix warning message
2024-10-23 12:06:49 +02:00
Jonas Kaninda 8e20e9595f refactor: update logger, fix warning message 2024-10-23 12:06:24 +02:00
jkaninda 02e3267237 Merge pull request #142 from jkaninda/refactor
refactor: update logger, fix warning message
2024-10-23 12:04:50 +02:00
Jonas Kaninda 448ef4d988 refactor: update logger, fix warning message 2024-10-23 12:04:19 +02:00
jkaninda 70ac78c2cd Merge pull request #141 from jkaninda/refactor
docs: update supported storage
2024-10-23 10:31:48 +02:00
Jonas Kaninda 72f5ef4839 docs: update supported storage 2024-10-23 10:31:24 +02:00
jkaninda 6a51f591a5 Merge pull request #138 from jkaninda/dependabot/go_modules/github.com/spf13/cobra-1.8.1
chore(deps): bump github.com/spf13/cobra from 1.8.0 to 1.8.1
2024-10-23 10:03:18 +02:00
jkaninda d55ade3c21 Merge pull request #139 from jkaninda/dependabot/go_modules/golang.org/x/crypto-0.28.0
chore(deps): bump golang.org/x/crypto from 0.18.0 to 0.28.0
2024-10-23 10:02:54 +02:00
dependabot[bot] cdbd6dcd6a chore(deps): bump golang.org/x/crypto from 0.18.0 to 0.28.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.18.0 to 0.28.0.
- [Commits](https://github.com/golang/crypto/compare/v0.18.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 08:01:21 +00:00
dependabot[bot] 307e18d9ff chore(deps): bump github.com/spf13/cobra from 1.8.0 to 1.8.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.8.0...v1.8.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 08:01:20 +00:00
jkaninda 8d366f0302 Merge pull request #140 from jkaninda/dependabot/go_modules/github.com/aws/aws-sdk-go-1.55.5
chore(deps): bump github.com/aws/aws-sdk-go from 1.55.3 to 1.55.5
2024-10-23 10:00:07 +02:00
jkaninda 05e32c3cc1 Merge pull request #137 from jkaninda/dependabot/docker/golang-1.23.2
chore(deps): bump golang from 1.22.5 to 1.23.2
2024-10-23 09:59:27 +02:00
dependabot[bot] edd13907d0 chore(deps): bump github.com/aws/aws-sdk-go from 1.55.3 to 1.55.5
Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.55.3 to 1.55.5.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.55.3...v1.55.5)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 07:51:28 +00:00
dependabot[bot] 7cb1c50927 chore(deps): bump golang from 1.22.5 to 1.23.2
Bumps golang from 1.22.5 to 1.23.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-23 07:51:07 +00:00
jkaninda f545704b02 Merge pull request #136 from jkaninda/refactor
Set up automated updates for Docker base images and Go packages
2024-10-23 09:50:23 +02:00
jkaninda 90f5391b24 Set up automated updates for Docker base images and Go packages 2024-10-23 09:50:09 +02:00
jkaninda ca241b4fef Merge pull request #135 from jkaninda/refactor
fix: add identifyFile
2024-10-23 09:37:56 +02:00
Jonas Kaninda 3911296921 fix: add identifyFile 2024-10-23 09:36:58 +02:00
jkaninda 8d04d276ba Merge pull request #134 from jkaninda/refactor
chore: integrate external storage module
2024-10-23 04:05:04 +02:00
Jonas Kaninda 221079e0ea chore: integrate external storage module 2024-10-23 04:04:38 +02:00
jkaninda 590b2d8bc6 Merge pull request #133 from jkaninda/refactor
Refactor
2024-10-22 17:24:09 +02:00
Jonas Kaninda d2aeb55ebc docs: update notification template 2024-10-22 17:22:45 +02:00
Jonas Kaninda 431be36210 refactor: create storage interface, refactor local, s3, ftp and ssh storage 2024-10-22 17:21:01 +02:00
jkaninda ef2c5c80cd Merge pull request #132 from jkaninda/refactor
Refactor
2024-10-21 08:41:50 +02:00
Jonas Kaninda 3a0137d6ea doc: update deployment using s3 storage 2024-10-21 08:35:57 +02:00
Jonas Kaninda 8afb5ace40 chore: add ssh, ftp to storage list 2024-10-21 08:33:59 +02:00
Jonas Kaninda 5569258a71 refactor: clean up Dockerfile 2024-10-21 08:26:59 +02:00
Jonas Kaninda f3ec395e37 refactor: clean up code 2024-10-21 08:22:06 +02:00
jkaninda ba432997c8 Merge pull request #131 from jkaninda/refactor
refactor: ineffectual assignment
2024-10-20 08:22:02 +02:00
Jonas Kaninda dc20ea9635 refactor: ineffectual assignment 2024-10-20 08:21:36 +02:00
jkaninda 40557af437 Merge pull request #130 from jkaninda/refactor
Refactor
2024-10-20 06:58:20 +02:00
Jonas Kaninda 1dcb9586a6 chore: add command usage error 2024-10-20 06:57:06 +02:00
Jonas Kaninda 2c6336e84a chore: add backup prune, replace period flag by BACKUP_RETENTION_DAYS environment variable 2024-10-20 06:52:36 +02:00
jkaninda c16ee3a492 Merge pull request #129 from jkaninda/refactor
chore: replace prune and keep-last flags by BACKUP_RETENTION_DAYS env…
2024-10-19 05:31:31 +02:00
Jonas Kaninda 3f7d28ea49 chore: replace prune and keep-last flags by BACKUP_RETENTION_DAYS env variable 2024-10-19 05:30:57 +02:00
jkaninda cea1ef9c3b Merge pull request #128 from jkaninda/refactor
doc: update using s3 storage example
2024-10-18 08:51:17 +02:00
Jonas Kaninda 56c271bc29 doc: update using s3 storage example 2024-10-18 08:51:04 +02:00
jkaninda 45c30dca5f Merge pull request #127 from jkaninda/refactor
Refactor
2024-10-15 16:57:44 +02:00
Jonas Kaninda b0ae212578 docs: update scheduled backup docker deployment example 2024-10-15 16:53:37 +02:00
Jonas Kaninda 6e2d3a9f21 add env.example 2024-10-15 16:52:25 +02:00
Jonas Kaninda dd314aa4cb chore: clean up Dockerfile 2024-10-15 16:50:37 +02:00
Jonas Kaninda 24ccdaa671 refactor: add default env variable value 2024-10-15 16:43:02 +02:00
jkaninda 45e3452376 Merge pull request #126 from jkaninda/refactor
docs: update readme
2024-10-13 14:53:31 +02:00
Jonas Kaninda 3527b4cdcd docs: update readme 2024-10-13 14:53:04 +02:00
jkaninda dc6fe2f4b9 Merge pull request #125 from jkaninda/refactor
chore: switch to encryptor module
2024-10-13 14:34:18 +02:00
Jonas Kaninda f0afc0f4e0 chore: switch to encryptor module 2024-10-13 14:33:54 +02:00
jkaninda 7d7c813bb0 Merge pull request #124 from jkaninda/template
docs: update restoration supported extensions
2024-10-12 11:14:08 +02:00
Jonas Kaninda 6b8491cdc0 docs: update restoration supported extensions 2024-10-12 11:13:45 +02:00
jkaninda a1dd6e3f58 Merge pull request #123 from jkaninda/template
docs: update email notification example
2024-10-12 10:47:28 +02:00
Jonas Kaninda 86ba3530c9 docs: update email notification example 2024-10-12 10:47:14 +02:00
jkaninda e1f3b15003 Merge pull request #122 from jkaninda/template
docs: update Kubernetes deployments
2024-10-10 21:22:08 +02:00
Jonas Kaninda 1577e92a66 docs: update Kubernetes deployments 2024-10-10 21:21:52 +02:00
jkaninda 7b67f88769 Merge pull request #121 from jkaninda/template
Template
2024-10-10 21:11:14 +02:00
Jonas Kaninda 043233dabe docs: update Kubernetes deployments 2024-10-10 21:10:40 +02:00
Jonas Kaninda d6652cfb75 chore: update github link 2024-10-10 21:03:07 +02:00
jkaninda 140ed608ab Merge pull request #120 from jkaninda/fix-dockerfile
fix: Dockerfile backup, restore, and migrate scripts since the migration of base image from Ubuntu to alpine
2024-10-10 10:03:44 +02:00
Jonas Kaninda 98211a27b8 fix: Dockerfile backup, restore, and migrate scripts since the migration of base image from Ubuntu to alpine 2024-10-10 10:02:42 +02:00
jkaninda 4e4d45e555 Merge pull request #119 from jkaninda/fix-notification
fix: fix multi backup s3 path
2024-10-10 05:51:46 +02:00
Jonas Kaninda 01e41acb5c fix: fix multi backup s3 path 2024-10-10 05:51:18 +02:00
jkaninda 3dce2017f8 Merge pull request #118 from jkaninda/fix-notification
fix: fix multi backup s3 path
2024-10-10 05:32:08 +02:00
Jonas Kaninda ed2f1b8d9c fix: fix multi backup s3 path 2024-10-10 05:31:18 +02:00
jkaninda b64875df21 Merge pull request #117 from jkaninda/fix-notification
docs: correct grammar in  receive-notification.md
2024-10-10 04:28:52 +02:00
Jonas Kaninda fc90507b3f docs: correct grammar in receive-notification.md 2024-10-10 04:28:02 +02:00
jkaninda df0efd24d3 Merge pull request #116 from jkaninda/fix-notification
chore: fix infinity calling Fatal, add a backup reference
2024-10-10 04:15:12 +02:00
Jonas Kaninda e5dd7e76ce chore: fix infinity calling Fatal, add a backup reference 2024-10-10 04:14:42 +02:00
jkaninda 12fbb67a09 Merge pull request #115 from jkaninda/email-notification
docs: update send notification
2024-10-09 22:38:35 +02:00
Jonas Kaninda df490af7b6 docs: update send notification 2024-10-09 22:38:07 +02:00
jkaninda d930c3e2f6 Merge pull request #114 from jkaninda/email-notification
feat: add email notification for failed and success backup
2024-10-09 22:32:44 +02:00
Jonas Kaninda e4258cb12e feat: add email notification for failed and success backup 2024-10-09 22:31:52 +02:00
jkaninda 4c44166921 Merge pull request #113 from jkaninda/develop
Develop
2024-10-09 12:51:15 +02:00
jkaninda 554df819ab Merge pull request #112 from jkaninda/multi-backup
docs: add mutli database backup example
2024-10-09 12:49:46 +02:00
Jonas Kaninda ca5633882e docs: add mutli database backup example 2024-10-09 12:45:55 +02:00
jkaninda c5cca82841 Merge pull request #111 from jkaninda/multi-backup
Add Multi database backup
2024-10-09 12:24:37 +02:00
Jonas Kaninda bbd5422089 ci: change Dockerfile path 2024-10-09 12:23:45 +02:00
Jonas Kaninda d72156f890 feat: add multi database backup 2024-10-09 12:23:14 +02:00
Jonas Kaninda 909a50dbe7 docs: update backup encryption example 2024-10-08 23:20:50 +02:00
Jonas Kaninda 94ceb71da2 docs: update backup encryption example 2024-10-08 23:05:10 +02:00
Jonas Kaninda fe05fe5110 feat: add encrypt backup using public key, migrate gpg to go gpg dependency 2024-10-08 23:02:46 +02:00
jkaninda dabba2050a Merge pull request #110 from jkaninda/refactor
chore: remove os.kill.signal
2024-10-05 10:42:55 +02:00
Jonas Kaninda 47e1ac407b chore: remove os.kill.signal 2024-10-05 10:41:46 +02:00
jkaninda 28f6ed3a82 Merge pull request #109 from jkaninda/refactor
fix: logging time
2024-10-05 10:40:11 +02:00
Jonas Kaninda 504926c7cd fix: logging time 2024-10-05 10:39:49 +02:00
jkaninda 737f473f92 Merge pull request #108 from jkaninda/refactor
Refactor
2024-10-03 18:19:12 +02:00
Jonas Kaninda 300d2a8205 chore: remove testDatabaseConnection function for scheduled mode 2024-10-03 18:18:47 +02:00
Jonas Kaninda a4ad0502cf chore: add storage type alt for smallcase and uppercase 2024-10-03 18:17:48 +02:00
jkaninda f344867edf Merge pull request #107 from jkaninda/refactor
docs: update configuration reference
2024-10-02 04:26:05 +02:00
Jonas Kaninda d774584f64 docs: update configuration reference 2024-10-02 04:25:35 +02:00
jkaninda 96927cd57e Merge pull request #106 from jkaninda/refactor
Refactor
2024-10-02 04:13:20 +02:00
Jonas Kaninda ceacfa1d9d docs: update ssh and ftp deployment example 2024-10-02 04:09:42 +02:00
Jonas Kaninda 9380a18b45 refactor: remove old arguments, refactor aws and ssh configuration 2024-10-02 04:07:14 +02:00
Jonas Kaninda d186071df9 Merge pull request #105 from jkaninda/refactor
chore: update app version
2024-09-30 17:49:21 +02:00
Jonas Kaninda 71429b0e1a chore: update app version 2024-09-30 17:48:56 +02:00
Jonas Kaninda 0bed86ded4 Merge pull request #104 from jkaninda/refactor
chore: add Time Zone
2024-09-30 17:45:38 +02:00
Jonas Kaninda e891801125 chore: add Time Zone 2024-09-30 17:44:45 +02:00
66 changed files with 5198 additions and 3002 deletions

No files matched your search

+30
View File
@@ -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
+87
View File
@@ -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
+10
View File
@@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
+9 -7
View File
@@ -1,7 +1,7 @@
name: Build
on:
push:
branches: ['develop']
branches: ['nightly']
env:
BUILDKIT_IMAGE: jkaninda/mysql-bkup
jobs:
@@ -10,23 +10,25 @@ jobs:
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
-
name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v3
uses: docker/build-push-action@v7
with:
push: true
file: "./docker/Dockerfile"
file: "./Dockerfile"
platforms: linux/amd64,linux/arm64,linux/arm/v7
build-args: |
appVersion=nightly
tags: |
"${{env.BUILDKIT_IMAGE}}:develop-${{ github.sha }}"
"${{ vars.BUILDKIT_IMAGE || 'jkaninda/mysql-bkup' }}:nightly"
+4 -4
View File
@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
@@ -32,14 +32,14 @@ jobs:
working-directory: docs
- name: Setup Pages
id: pages
uses: actions/configure-pages@v2
uses: actions/configure-pages@v6
- name: Build with Jekyll
working-directory: docs
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env:
JEKYLL_ENV: production
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
uses: actions/upload-pages-artifact@v5
with:
path: 'docs/_site/'
@@ -52,4 +52,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v5
+24
View File
@@ -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
+25 -12
View File
@@ -14,18 +14,18 @@ jobs:
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
-
name: Login to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR
uses: docker/login-action@v2
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -34,16 +34,29 @@ jobs:
name: Get the tag name
id: get_tag_name
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
uses: docker/build-push-action@v3
uses: docker/build-push-action@v7
with:
push: true
file: "./docker/Dockerfile"
file: "./Dockerfile"
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: |
"${{env.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
"${{env.BUILDKIT_IMAGE}}:latest"
"ghcr.io/${{env.BUILDKIT_IMAGE}}:${{ env.TAG_NAME }}"
"ghcr.io/${{env.BUILDKIT_IMAGE}}:latest"
build-args: |
appVersion=${{ env.TAG_NAME }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+52
View File
@@ -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 ./...
+55
View File
@@ -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
View File
@@ -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"]
+393 -133
View File
@@ -1,187 +1,447 @@
# MySQL Backup
MySQL Backup is a Docker container image that can be used to backup, restore and migrate MySQL database. It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, FTP and SSH compatible storage.
It also supports __encrypting__ your backups using GPG.
# MYSQL-BKUP
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, FTP or SSH compatible storage.
It also supports database __encryption__ using GPG.
**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.
[![Tests](https://github.com/jkaninda/mysql-bkup/actions/workflows/tests.yml/badge.svg)](https://github.com/jkaninda/mysql-bkup/actions/workflows/tests.yml)
[![Build](https://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml/badge.svg)](https://github.com/jkaninda/mysql-bkup/actions/workflows/release.yml)
[![Go Report](https://goreportcard.com/badge/github.com/jkaninda/mysql-bkup)](https://goreportcard.com/report/github.com/jkaninda/mysql-bkup)
![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/jkaninda/mysql-bkup?style=flat-square)
![Docker Pulls](https://img.shields.io/docker/pulls/jkaninda/mysql-bkup?style=flat-square)
<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>
Successfully tested on:
- Docker
- Docker in Swarm mode
- Kubernetes
- OpenShift
Documentation: <https://jkaninda.github.io/mysql-bkup>
## 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)
- [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 remote server
## Quickstart
### Simple backup using Docker CLI
To run a one time backup, bind your local volume to `/backup` in the container and run the `backup` command:
Back up the database `shop` into `./backup`:
```shell
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 backup -d database_name
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 backup -d shop
```
Alternatively, pass a `--env-file` in order to use a full config as described below.
The last line of the output names the backup:
```yaml
docker run --rm --network your_network_name \
--env-file your-env-file \
-v $PWD/backup:/backup/ \
jkaninda/mysql-bkup backup -d database_name
```
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
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: backup
command: backup -d shop --cron-expression "@daily"
volumes:
- ./backup:/backup
environment:
- DB_PORT=3306
- DB_HOST=mysql
- 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:
- DB_USER=backup
- DB_PASSWORD_FILE=/run/secrets/db_password
- BACKUP_RETENTION_DAYS=7
- GPG_PASSPHRASE=change-me
```
### Docker recurring backup
```shell
docker run --rm --network network_name \
-v $PWD/backup:/backup/ \
-e "DB_HOST=hostname" \
-e "DB_USERNAME=user" \
-e "DB_PASSWORD=password" \
jkaninda/mysql-bkup backup -d dbName --cron-expression "@every 1m"
```
See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
## Deploy on Kubernetes
For Kubernetes, you don't need to run it in scheduled mode. You can deploy it as Job or CronJob.
### Simple Kubernetes backup Job :
### Kubernetes CronJob
```yaml
apiVersion: batch/v1
kind: Job
kind: CronJob
metadata:
name: backup-job
name: mysql-bkup
spec:
ttlSecondsAfterFinished: 100
template:
schedule: "0 1 * * *"
jobTemplate:
spec:
containers:
- 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
command:
- /bin/sh
- -c
- backup -d dbname
resources:
limits:
memory: "128Mi"
cpu: "500m"
env:
- name: DB_HOST
value: "mysql"
- name: DB_USERNAME
value: "user"
- name: DB_PASSWORD
value: "password"
volumeMounts:
- mountPath: /backup
name: backup
volumes:
- name: backup
hostPath:
path: /home/toto/backup # directory location on host
type: Directory # this field is optional
restartPolicy: Never
template:
spec:
restartPolicy: Never
containers:
- name: mysql-bkup
image: jkaninda/mysql-bkup
args: ["backup", "-d", "shop", "--storage", "s3", "--path", "/mysql"]
envFrom:
- secretRef:
name: mysql-bkup # DB_HOST, DB_USER, DB_PASSWORD, AWS_* ...
```
## 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`:
## 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
```
docker pull jkaninda/mysql-bkup
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
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 `alpine` 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.
## PostgreSQL
Looking for PostgreSQL? See [pg-bkup](https://github.com/jkaninda/pg-bkup).
## License
This project is licensed under the MIT License. See the LICENSE file for details.
## Authors
**Jonas Kaninda**
- <https://github.com/jkaninda>
## Copyright
-39
View File
@@ -1,39 +0,0 @@
// Package cmd /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
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("storage", "s", "local", "Storage. local or s3")
BackupCmd.PersistentFlags().StringP("path", "P", "", "AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup`")
BackupCmd.PersistentFlags().StringP("mode", "m", "default", "Execution mode. | Deprecated")
BackupCmd.PersistentFlags().StringP("period", "", "", "Schedule period time | Deprecated")
BackupCmd.PersistentFlags().StringP("cron-expression", "", "", "Backup cron expression")
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")
}
-27
View File
@@ -1,27 +0,0 @@
// Package cmd /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package cmd
import (
"github.com/jkaninda/mysql-bkup/pkg"
"github.com/jkaninda/mysql-bkup/utils"
"github.com/spf13/cobra"
)
var MigrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate database from a source database to a target database",
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
pkg.StartMigration(cmd)
} else {
utils.Fatal("Error, no argument required")
}
},
}
-30
View File
@@ -1,30 +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")
RestoreCmd.PersistentFlags().StringP("storage", "s", "local", "Storage. local or s3")
RestoreCmd.PersistentFlags().StringP("path", "P", "", "AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup`")
}
-41
View File
@@ -1,41 +0,0 @@
// Package cmd /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@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("dbname", "d", "", "Database name")
rootCmd.AddCommand(VersionCmd)
rootCmd.AddCommand(BackupCmd)
rootCmd.AddCommand(RestoreCmd)
rootCmd.AddCommand(MigrateCmd)
}
-28
View File
@@ -1,28 +0,0 @@
// Package cmd /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package cmd
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()
}
-76
View File
@@ -1,76 +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 alpine:3.20.3
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_S3_PATH=""
ENV AWS_REGION="us-west-2"
ENV AWS_DISABLE_SSL="false"
ENV GPG_PASSPHRASE=""
ENV SSH_USER=""
ENV SSH_PASSWORD=""
ENV SSH_HOST_NAME=""
ENV SSH_IDENTIFY_FILE=""
ENV SSH_PORT=22
ENV REMOTE_PATH=""
ENV FTP_HOST_NAME=""
ENV FTP_PORT=21
ENV FTP_USER=""
ENV FTP_PASSWORD=""
ENV TARGET_DB_HOST=""
ENV TARGET_DB_PORT=3306
ENV TARGET_DB_NAME=""
ENV TARGET_DB_USERNAME=""
ENV TARGET_DB_PASSWORD=""
ENV VERSION="v1.2.9"
ENV BACKUP_CRON_EXPRESSION=""
ENV TG_TOKEN=""
ENV TG_CHAT_ID=""
ARG WORKDIR="/config"
ARG BACKUPDIR="/backup"
ARG BACKUP_TMP_DIR="/tmp/backup"
LABEL author="Jonas Kaninda"
RUN apk add --no-cache mysql-client mariadb-connector-c gnupg
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 chmod 777 $WORKDIR
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
# Create backup script and make it executable
RUN echo '#!/bin/sh\n/usr/local/bin/mysql-bkup backup "$@"' > /usr/local/bin/backup && \
chmod +x /usr/local/bin/backup
# Create restore script and make it executable
RUN echo '#!/bin/sh\n/usr/local/bin/mysql-bkup restore "$@"' > /usr/local/bin/restore && \
chmod +x /usr/local/bin/restore
# Create migrate script and make it executable
RUN echo '#!/bin/sh\n/usr/local/bin/mysql-bkup migrate "$@"' > /usr/local/bin/migrate && \
chmod +x /usr/local/bin/migrate
WORKDIR $WORKDIR
ENTRYPOINT ["/usr/local/bin/mysql-bkup"]
+1 -1
View File
@@ -20,7 +20,7 @@ description: >- # this means to ignore newlines until "baseurl:"
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
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
github_username: jkaninda
+72
View File
@@ -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.
+61
View File
@@ -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.
+47 -16
View File
@@ -4,41 +4,72 @@ layout: default
parent: How Tos
nav_order: 4
---
# Backup to FTP remote server
# Backup to FTP Remote Server
As described for SSH backup section, to change the storage of your backup and use FTP Remote server as storage. You need to add `--storage ftp`.
You need to add the full remote path by adding `--path /home/jkaninda/backups` flag or using `REMOTE_PATH` environment variable.
To store your backups on an FTP remote server, you can configure the backup process to use the `--storage ftp` option.
{: .note }
These environment variables are required for SSH backup `FTP_HOST_NAME`, `FTP_USER`, `REMOTE_PATH`, `FTP_PORT` or `FTP_PASSWORD`.
This section explains how to set up and configure FTP-based backups.
```yml
---
## 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, 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.
# 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=postgres
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
## FTP config
- FTP_HOST_NAME="hostname"
## FTP Configuration
- FTP_HOST=hostname
- FTP_PORT=21
- FTP_USER=user
- FTP_PASSWORD=password
- REMOTE_PATH=/home/jkaninda/backups
# pg-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:
- 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`.
+108 -116
View File
@@ -4,131 +4,123 @@ layout: default
parent: How Tos
nav_order: 2
---
# Backup to AWS S3
# Backup to AWS S3
{: .note }
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.
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.
---
## Backup to S3
## Configuration Steps
```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: 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
networks:
- web
networks:
web:
```
1. **Specify the Storage Type**
Add the `--storage s3` flag to your backup command.
### Recurring backups to S3
2. **Set the S3 Path**
Optionally, specify a custom folder within your S3 bucket where backups will be stored using the `--path` flag.
Example: `--path /my-custom-path`.
As explained above, you need just to add AWS environment variables and specify the storage type `--storage s3`.
In case you need to use recurring backups, you can use `--cron-expression "0 1 * * *"` flag or `BACKUP_CRON_EXPRESSION=0 1 * * *` as described below.
3. **Required Environment Variables**
The following environment variables are mandatory for S3-based backups:
```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: backup --storage s3 -d my-database --cron-expression "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
# - BACKUP_CRON_EXPRESSION=0 1 * * * # Optional
## 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:
```
- `AWS_S3_ENDPOINT`: The S3 endpoint URL (e.g., `https://s3.amazonaws.com`).
- `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"`).
## Deploy on Kubernetes
---
For Kubernetes, you don't need to run it in scheduled mode. You can deploy it as CronJob.
## Example Configuration
### Simple Kubernetes CronJob usage:
Below is an example `docker-compose.yml` configuration for backing up to AWS S3:
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: bkup-job
spec:
schedule: "0 1 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: mysql-bkup
image: jkaninda/mysql-bkup
command:
- /bin/sh
- -c
- backup -s s3 --path /custom_path
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
```
services:
mysql-bkup:
# In production, lock your image tag to a specific release version
# instead of using `latest`. Check https://github.com/jkaninda/pg-bkup/releases
# for available releases.
image: jkaninda/mysql-bkup
container_name: pg-bkup
command: backup --storage s3 -d database --path /my-custom-path
environment:
- DB_PORT=5432
- DB_HOST=postgres
- 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: 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:
```
---
## 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.
+114 -123
View File
@@ -1,141 +1,132 @@
---
title: Backup to SSH
title: Backup to SSH or SFTP
layout: default
parent: How Tos
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 your backup and use SSH Remote server 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 `REMOTE_PATH` environment variable.
---
{: .note }
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.
## Configuration Steps
```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: backup --storage remote -d database
volumes:
- ./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
- 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
1. **Specify the Storage Type**
Add the `--storage ssh` or `--storage remote` flag to your backup command.
# mysql-bkup container must be connected to the same network with your database
networks:
- web
networks:
web:
```
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:
### Recurring backups to SSH remote server
- `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.
As explained above, you need just to add required environment variables and specify the storage type `--storage ssh`.
You can use `--cron-expression "* * * * *"` or `BACKUP_CRON_EXPRESSION=0 1 * * *` as described below.
{: .note }
**Security Recommendation**: Using a private key (`SFTP_IDENTITY_FILE`) is strongly recommended over password-based authentication (`SFTP_PASSWORD`) for better security.
```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: backup -d database --storage ssh --cron-expression "0 1 * * *"
volumes:
- ./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
- REMOTE_PATH=/home/jkaninda/backups
- SSH_IDENTIFY_FILE=/tmp/id_ed25519
# - BACKUP_CRON_EXPRESSION=0 1 * * * # Optional
## 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:
```
---
## Deploy on Kubernetes
## Example Configuration
For Kubernetes, you don't need to run it in scheduled mode.
You can deploy it as CronJob.
Simple Kubernetes CronJob usage:
Below is an example `docker-compose.yml` configuration for backing up to an SSH remote server:
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: bkup-job
spec:
schedule: "0 1 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: mysql-bkup
image: jkaninda/mysql-bkup
command:
- /bin/sh
- -c
- backup -s ssh
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: REMOTE_PATH
value: "/home/jkaninda/backups"
- name: AWS_ACCESS_KEY
value: "xxxx"
- name: SSH_IDENTIFY_FILE
value: "/tmp/id_ed25519"
restartPolicy: Never
```
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 remote -d database
volumes:
- ./id_ed25519:/tmp/id_ed25519
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USER=username
- DB_PASSWORD=password
## SSH Configuration
- SFTP_HOST=hostname
- SFTP_PORT=22
- SFTP_USER=user
- REMOTE_PATH=/home/jkaninda/backups
- SFTP_IDENTITY_FILE=/tmp/id_ed25519
- SFTP_KNOWN_HOSTS=/config/known_hosts
## Optional: Use password instead of private key (not recommended)
#- SFTP_PASSWORD=password
# Ensure the mysql-bkup container is connected to the same network as your database
networks:
- web
networks:
web:
```
---
## Recurring Backups to SSH Remote Server
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
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 --storage ssh --cron-expression "@daily"
volumes:
- ./id_ed25519:/tmp/id_ed25519
environment:
- DB_PORT=3306
- DB_HOST=postgres
- DB_NAME=database
- DB_USER=username
- DB_PASSWORD=password
## SSH Configuration
- SFTP_HOST=hostname
- SFTP_PORT=22
- SFTP_USER=user
- REMOTE_PATH=/home/jkaninda/backups
- SFTP_IDENTITY_FILE=/tmp/id_ed25519
- SFTP_KNOWN_HOSTS=/config/known_hosts
## Optional: Delete old backups after a specified number of days
#- BACKUP_RETENTION_DAYS=7
## Optional: Use password instead of private key (not recommended)
#- SFTP_PASSWORD=password
# 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.
- **Security**: Always prefer private key authentication (`SFTP_IDENTITY_FILE`) over password-based authentication (`SFTP_PASSWORD`) for enhanced security.
---
+65 -32
View File
@@ -5,26 +5,35 @@ parent: How Tos
nav_order: 1
---
# Backup database
# Backup Database
To backup the database, you need to add `backup` command.
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 }
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.
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.
## Example: Basic Backup Configuration
```yml
Below is an example `docker-compose.yml` configuration for backing up a database:
```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.
# 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
@@ -34,51 +43,75 @@ services:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- 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:
- web
networks:
web:
```
### Backup using Docker CLI
---
```shell
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 backup -d database_name
## Backup Using Docker CLI
You can also run backups directly using the Docker CLI:
```bash
docker run --rm --network your_network_name \
-v $PWD/backup:/backup/ \
-e "DB_HOST=dbhost" \
-e "DB_USER=username" \
-e "DB_PASSWORD=password" \
jkaninda/pg-bkup backup -d database_name
```
In case you need to use recurring backups, you can use `--cron-expression "0 1 * * *"` flag or `BACKUP_CRON_EXPRESSION=0 1 * * *` 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:
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.
# 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 --cron-expression "0 1 * * *"
command: backup -d database --cron-expression @midnight
volumes:
- ./backup:/backup
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
- BACKUP_CRON_EXPRESSION=0 1 * * *
# 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:
- web
networks:
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.
+235 -169
View File
@@ -5,12 +5,17 @@ parent: How Tos
nav_order: 9
---
## Deploy on Kubernetes
# Deploy on Kubernetes
To deploy MySQL Backup on Kubernetes, you can use Job to backup or Restore your database.
For recurring backup you can use CronJob, you don't need to run it in scheduled mode. as described bellow.
To deploy MySQL Backup on Kubernetes, you can use a `Job` for one-time backups or restores, and a `CronJob` for recurring backups.
## Backup to S3 storage
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
@@ -21,48 +26,53 @@ spec:
template:
spec:
containers:
- 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
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: "dbname"
- name: DB_USERNAME
value: "username"
# 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"
- 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
---
## 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
@@ -75,19 +85,14 @@ spec:
spec:
containers:
- 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.
# 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
- bkup
- backup
- --storage
- ssh
- --disable-compression
- backup --storage ssh --disable-compression
resources:
limits:
memory: "128Mi"
@@ -99,29 +104,33 @@ spec:
value: ""
- name: DB_NAME
value: "dbname"
- name: DB_USERNAME
value: "username"
# Please use secret!
- name: DB_USER
value: "postgres"
# Use Kubernetes Secrets for sensitive data like passwords
- name: DB_PASSWORD
value: ""
- name: SSH_HOST_NAME
- name: SFTP_HOST
value: "xxx"
- name: SSH_PORT
- name: SFTP_PORT
value: "22"
- name: SSH_USER
- name: SFTP_USER
value: "xxx"
- name: SSH_PASSWORD
- name: SFTP_PASSWORD
value: "xxxx"
- name: SSH_REMOTE_PATH
- name: REMOTE_PATH
value: "/home/toto/backup"
# Optional, required if you want to encrypt your 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
@@ -133,52 +142,51 @@ spec:
spec:
containers:
- 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.
# 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
- bkup
- restore
- --storage
- ssh
- --file store_20231219_022941.sql.gz
- 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_USERNAME
value: "username"
# Please use secret!
- name: DB_PASSWORD
value: ""
- name: SSH_HOST_NAME
value: "xxx"
- name: SSH_PORT
value: "22"
- name: SSH_USER
value: "xxx"
- name: SSH_PASSWORD
value: "xxxx"
- name: SSH_REMOTE_PATH
value: "/home/xxxx/backup"
# Optional, required if your backup was encrypted
#- name: GPG_PASSPHRASE
# value: "xxxx"
- 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
---
## 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
@@ -193,52 +201,51 @@ spec:
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
- bkup
- backup
- --storage
- ssh
- --disable-compression
- 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: "username"
- name: DB_USERNAME
value: "username"
# Please use secret!
- name: DB_PASSWORD
value: ""
- name: SSH_HOST_NAME
value: "xxx"
- name: SSH_PORT
value: "xxx"
- name: SSH_USER
value: "jkaninda"
- name: SSH_REMOTE_PATH
value: "/home/jkaninda/backup"
- name: SSH_PASSWORD
value: "password"
# Optional, required if you want to encrypt your backup
#- name: GPG_PASSPHRASE
# value: "xxx"
- 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
---
This image also supports Kubernetes security context, you can run it in Rootless environment.
It has been tested on Openshift, it works well.
Deployment on OpenShift is supported, you need to remove `securityContext` section on your yaml file.
## 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
@@ -256,48 +263,107 @@ spec:
runAsGroup: 3000
fsGroup: 2000
containers:
# 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.
- name: mysql-bkup
image: jkaninda/mysql-bkup
command:
- /bin/sh
- -c
- bkup
- 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: "xxx"
- name: DB_USERNAME
value: "xxx"
# Please use secret!
- name: DB_PASSWORD
value: ""
- name: SSH_HOST_NAME
value: "xxx"
- name: SSH_PORT
value: "22"
- name: SSH_USER
value: "jkaninda"
- name: SSH_REMOTE_PATH
value: "/home/jkaninda/backup"
- name: SSH_PASSWORD
value: "password"
# Optional, required if you want to encrypt your backup
- 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.
+6
View File
@@ -0,0 +1,6 @@
---
title: Update deprecated configurations
layout: default
parent: How Tos
nav_order: 11
---
+104 -22
View File
@@ -4,35 +4,35 @@ layout: default
parent: How Tos
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 }
To restore an encrypted backup, you need to provide the same GPG passphrase or key used during backup process.
To restore an encrypted backup, you must provide the same GPG passphrase or private key used during the backup process.
- GPG home directory `/config/gnupg`
- Cipher algorithm `aes256`
-
To decrypt manually, you need to install `gnupg`
---
### Decrypt backup
## Key Features
```shell
gpg --batch --passphrase "my-passphrase" \
--output database_20240730_044201.sql.gz \
--decrypt database_20240730_044201.sql.gz.gpg
```
- **Cipher Algorithm**: `aes256`
- **Automatic Restoration**: Backups encrypted with a GPG passphrase can be restored automatically without manual decryption.
- **Manual Decryption**: Backups encrypted with a GPG public key require manual decryption before restoration.
### 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:
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.
# 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
@@ -42,13 +42,95 @@ services:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
## Required to encrypt backup
- 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:
- web
networks:
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`.
+58 -80
View File
@@ -5,127 +5,105 @@ parent: How Tos
nav_order: 10
---
# Migrate database
# Migrate Database
To migrate the database, you need to add `migrate` command.
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 Mysql backup has another great feature: migrating your database from a source database to a target.
As you know, to restore a database from a source to a target database, you need 2 operations: which is to start by backing up the source database and then restoring the source backed database to the target database.
Instead of proceeding like that, you can use the integrated feature `(migrate)`, which will help you migrate your database by doing only one operation.
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, please backup your target database before this action.
The `migrate` operation is **irreversible**. Always back up your target database before performing this action.
### Docker compose
```yml
---
## 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, 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.
# 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
## Source Database
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
## Target database
## Target Database
- TARGET_DB_HOST=target-mysql
- TARGET_DB_PORT=3306
- TARGET_DB_NAME=dbname
- TARGET_DB_USERNAME=username
- TARGET_DB_USER=username
- TARGET_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:
- web
networks:
web:
```
---
### Migrate database using Docker CLI
## Migrate Database Using Docker CLI
You can also run the migration directly using the Docker CLI. Below is an example:
```
## Source database
DB_HOST=mysql
### 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_USERNAME=username
DB_USER=username
DB_PASSWORD=password
## Taget database
## Target Database
TARGET_DB_HOST=target-mysql
TARGET_DB_PORT=3306
TARGET_DB_NAME=dbname
TARGET_DB_USERNAME=username
TARGET_DB_USER=username
TARGET_DB_PASSWORD=password
```
```shell
docker run --rm --network your_network_name \
--env-file your-env
-v $PWD/backup:/backup/ \
jkaninda/mysql-bkup migrate
### Run the Migration
```bash
docker run --rm --network your_network_name \
--env-file your-env \
-v $PWD/backup:/backup/ \
jkaninda/mysql-bkup migrate
```
## Kubernetes
---
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: migrate-db
spec:
ttlSecondsAfterFinished: 100
template:
spec:
containers:
- 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
command:
- /bin/sh
- -c
- migrate
resources:
limits:
memory: "128Mi"
cpu: "500m"
env:
## Source Database
- name: DB_HOST
value: "mysql"
- name: DB_PORT
value: "3306"
- name: DB_NAME
value: "dbname"
- name: DB_USERNAME
value: "username"
- name: DB_PASSWORD
value: "password"
## Target Database
- name: TARGET_DB_HOST
value: "target-mysql"
- name: TARGET_DB_PORT
value: "3306"
- name: TARGET_DB_NAME
value: "dbname"
- name: TARGET_DB_USERNAME
value: "username"
- name: TARGET_DB_PASSWORD
value: "password"
restartPolicy: Never
```
## 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.
+103
View File
@@ -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:
```
---
+236
View File
@@ -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.
+41 -61
View File
@@ -5,91 +5,71 @@ parent: How Tos
nav_order: 6
---
# Restore database from S3 storage
# Restore Database from S3 Storage
To restore the database, you need to add `restore` command 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 }
It supports __.sql__ and __.sql.gz__ compressed file.
- `.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)
### 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:
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.
# 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: restore --storage s3 -d my-database -f store_20231219_022941.sql.gz --path /my-custom-path
volumes:
- ./backup:/backup
- ./backup:/backup # Mount the directory for local operations (if needed)
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
## AWS configurations
## AWS S3 Configuration
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
- AWS_S3_BUCKET_NAME=backup
- AWS_REGION="us-west-2"
- 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
## 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:
```
## Restore on Kubernetes
---
Simple Kubernetes restore Job:
## Key Notes
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: restore-db
spec:
template:
spec:
containers:
- name: mysql-bkup
image: jkaninda/mysql-bkup
command:
- /bin/sh
- -c
- 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: Never
backoffLimit: 4
```
- **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.
- **Encrypted Backups**: If the backup is encrypted with GPG, ensure the `GPG_PASSPHRASE` environment variable is set for automatic decryption.
- **S3 Alternatives**: For S3-compatible storage like Minio, set `AWS_DISABLE_SSL` and `AWS_FORCE_PATH_STYLE` as needed.
- **Network Configuration**: Ensure the `pg-bkup` container is connected to the same network as your database.
+48 -66
View File
@@ -4,90 +4,72 @@ layout: default
parent: How Tos
nav_order: 7
---
# Restore database from SSH remote server
To restore the database from your remote server, you need to add `restore` command and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
# Restore Database from SSH Remote Server
{: .note }
It supports __.sql__ and __.sql.gz__ compressed file.
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:
### 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:
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.
# 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: restore --storage ssh -d my-database -f store_20231219_022941.sql.gz --path /home/jkaninda/backups
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:
- DB_PORT=3306
- DB_HOST=postgres
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=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
## SSH Configuration
- SFTP_HOST=hostname
- SFTP_PORT=22
- SFTP_USER=user
- REMOTE_PATH=/home/jkaninda/backups
- SFTP_IDENTITY_FILE=/tmp/id_ed25519
- SFTP_KNOWN_HOSTS=/config/known_hosts
## Optional: Use password instead of private key (not recommended)
#- SFTP_PASSWORD=password
# Ensure the mysql-bkup container is connected to the same network as your database
networks:
- web
networks:
web:
```
## Restore on Kubernetes
Simple Kubernetes restore Job:
---
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: restore-db
spec:
template:
spec:
containers:
- name: mysql-bkup
image: jkaninda/mysql-bkup
command:
- /bin/sh
- -c
- 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
backoffLimit: 4
```
## Key Notes
- **Supported File Formats**: The restore process supports `.sql`, `.sql.gz`, `.sql.gpg`, and `.sql.gz.gpg` files.
- **SSH Path**: Use the `--path` flag to specify the folder on the SSH remote server where the backup file is located.
- **Encrypted Backups**: If the backup is encrypted with GPG, ensure the `GPG_PASSPHRASE` environment variable is set for automatic decryption.
- **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`).
- **Network Configuration**: Ensure the `mysql-bkup` container is connected to the same network as your database.
+39 -15
View File
@@ -5,36 +5,60 @@ parent: How Tos
nav_order: 5
---
# Restore database
To restore the database, you need to add `restore` command and specify the file to restore by adding `--file store_20231219_022941.sql.gz`.
# Restore Database
{: .note }
It supports __.sql__ and __.sql.gz__ compressed file.
To restore a MySQL database, use the `restore` command and specify the backup file to restore with the `--file` flag.
### 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:
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.
# 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: restore -d database -f store_20231219_022941.sql.gz
volumes:
- ./backup:/backup
- ./backup:/backup # Mount the directory containing the backup file
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_HOST=postgres
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- 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:
- web
networks:
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.
+65 -131
View File
@@ -6,158 +6,92 @@ nav_order: 1
# About mysql-bkup
{:.no_toc}
MySQL Backup is a Docker container image that can be used to backup, restore and migrate MySQL database. It supports local storage, AWS S3 or any S3 Alternatives for Object Storage, FTP and SSH remote 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 }
---
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 database __encryption__ using GPG.
{: .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
---
## 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 `backup` command:
```shell
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 backup -d database_name
```
Alternatively, pass a `--env-file` in order to use a full config as described below.
```yaml
docker run --rm --network your_network_name \
--env-file your-env-file \
-v $PWD/backup:/backup/ \
jkaninda/mysql-bkup backup -d database_name
```
### 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: backup
volumes:
- ./backup:/backup
environment:
- DB_PORT=3306
- DB_HOST=mysql
- 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:
```
### Docker recurring backup
```shell
docker run --rm --network network_name \
-v $PWD/backup:/backup/ \
-e "DB_HOST=hostname" \
-e "DB_USERNAME=user" \
-e "DB_PASSWORD=password" \
jkaninda/mysql-bkup backup -d dbName --cron-expression "@every 1m"
```
See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
## Kubernetes
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: backup-job
spec:
ttlSecondsAfterFinished: 100
template:
spec:
containers:
- 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
command:
- /bin/sh
- -c
- backup -d dbname
resources:
limits:
memory: "128Mi"
cpu: "500m"
env:
- name: DB_HOST
value: "mysql"
- name: DB_USERNAME
value: "user"
- name: DB_PASSWORD
value: "password"
volumeMounts:
- mountPath: /backup
name: backup
volumes:
- name: backup
hostPath:
path: /home/toto/backup # directory location on host
type: Directory # this field is optional
restartPolicy: Never
```
## 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`:
```
```bash
docker pull jkaninda/mysql-bkup
docker pull ghcr.io/jkaninda/mysql-bkup
```
Documentation references Docker Hub, but all examples will work using ghcr.io just as well.
While the documentation references Docker Hub, all examples work seamlessly with `ghcr.io`.
## 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
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 `alpine` 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.
- **Lightweight:** Written in Go, the image is optimized for performance and minimal resource usage.
- **Multi-Architecture Support:** Supports `arm64` and `arm/v7` architectures.
- **Docker Swarm Support:** Fully compatible with Docker in Swarm mode.
- **Kubernetes Support:** Designed to work seamlessly with Kubernetes.
+212
View File
@@ -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 -107
View File
@@ -1,135 +1,301 @@
---
title: Configuration Reference
layout: default
nav_order: 2
nav_order: 3
---
# Configuration reference
# Configuration Reference
Backup, restore and migrate 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 |
|-----------------------|--------|----------------------------------------------------------------------------------------|
| mysql-bkup | bkup | CLI utility |
| backup | | Backup database operation |
| restore | | Restore database operation |
| migrate | | Migrate database from one instance to another one |
| --storage | -s | Storage. local or s3 (default: local) |
| --file | -f | File name for restoration |
| --path | | AWS S3 path without file name. eg: /custom_path or ssh remote path `/home/foo/backup` |
| --dbname | -d | Database name |
| --port | -p | Database port (default: 3306) |
| --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 |
| --cron-expression | | Backup cron expression, eg: (* * * * *) or @daily |
| --help | -h | Print this help message and exit |
| --version | -V | Print version information and exit |
## Exit codes
| Code | Meaning |
|---|---|
| `0` | success |
| `1` | failure |
| `2` | configuration error |
| `3` | database connection or authentication |
| `4` | storage |
| `5` | encryption or decryption |
## Environment variables
| Name | Requirement | Description |
|------------------------|---------------------------------------------------------------|------------------------------------------------------|
| 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) |
| GPG_PASSPHRASE | Optional, required to encrypt and restore backup | GPG passphrase |
| BACKUP_CRON_EXPRESSION | Optional if it was provided from the `--cron-expression` flag | Backup cron expression for docker in scheduled mode |
| 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 |
| REMOTE_PATH | Optional, required for SSH or FTP storage | remote path (/home/toto/backup) |
| FTP_HOST_NAME | Optional, required for FTP storage | FTP host name |
| FTP_PORT | Optional, required for FTP storage | FTP server port number |
| FTP_USER | Optional, required for FTP storage | FTP user |
| FTP_PASSWORD | Optional, required for FTP storage | FTP user password |
| TARGET_DB_HOST | Optional, required for database migration | Target database host |
| TARGET_DB_PORT | Optional, required for database migration | Target database port |
| TARGET_DB_NAME | Optional, required for database migration | Target database name |
| TARGET_DB_USERNAME | Optional, required for database migration | Target database username |
| TARGET_DB_PASSWORD | Optional, required for database migration | Target database password |
| TG_TOKEN | Optional, required for Telegram notification | Telegram token (`BOT-ID:BOT-TOKEN`) |
| TG_CHAT_ID | Optional, required for Telegram notification | Telegram Chat ID |
### storage
---
## Run in Scheduled mode
| 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) |
This image can be run as CronJob in Kubernetes for a regular backup which makes deployment on Kubernetes easy as Kubernetes has CronJob resources.
For Docker, you need to run it in scheduled mode by adding `--cron-expression "* * * * *"` flag or by defining `BACKUP_CRON_EXPRESSION=0 1 * * *` environment variable.
### general
## 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)
- 2: Hours (0-23)
- 3: Day (0-31)
- 4: Month (0-12 [12 == December])
- 5: Day of the week(0-7 [7 or 0 == sunday])
| 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) |
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
* * * * * 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
- - - - -
| | | | |
| | | | ----- 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
```conf
*/30 * * * *
```
> “At minute 0.” every hour
```conf
0 * * * *
```
> “At 01:00.” every day
```conf
0 1 * * *
```
## Predefined schedules
You may use one of several pre-defined schedules in place of a cron expression.
| 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 may also schedule backup task at fixed intervals, starting at the time it's added or cron is run. This is supported by formatting the cron spec like this:
@every <duration>
where "duration" is a string accepted by time.
```
For example, "@every 1h30m10s" would indicate a schedule that activates after 1 hour, 30 minutes, 10 seconds, and then every interval after that.
- Example: `@every 1h30m10s` runs the backup every 1 hour, 30 minutes, and 10 seconds.
+47
View File
@@ -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.
+5 -4
View File
@@ -6,21 +6,22 @@ services:
# for a list of available releases.
image: jkaninda/mysql-bkup
container_name: mysql-bkup
command: backup --storage s3 -d my-database"
command: backup --storage s3 -d my-database
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
## AWS configurations
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
- AWS_S3_BUCKET_NAME=backup
- AWS_REGION="us-west-2"
- 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"
- 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
networks:
- web
+1 -2
View File
@@ -1,4 +1,3 @@
version: "3"
services:
mysql-bkup:
# In production, it is advised to lock your image tag to a proper
@@ -11,7 +10,7 @@ services:
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_USERNAME=userName
- DB_USER=userName
- DB_PASSWORD=${DB_PASSWORD}
# See: https://jkaninda.github.io/mysql-bkup/reference/#predefined-schedules
- BACKUP_CRON_EXPRESSION=@daily #@every 5m|@weekly | @monthly |0 1 * * *
+4 -3
View File
@@ -11,16 +11,17 @@ services:
- DB_PORT=3306
- DB_HOST=mysql
- DB_NAME=database
- DB_USERNAME=username
- DB_USER=username
- DB_PASSWORD=password
## AWS configurations
- AWS_S3_ENDPOINT=https://s3.amazonaws.com
- AWS_S3_BUCKET_NAME=backup
- AWS_REGION="us-west-2"
- 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"
- 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
+1 -2
View File
@@ -1,4 +1,3 @@
version: "3"
services:
mysql-bkup:
image: jkaninda/mysql-bkup
@@ -9,5 +8,5 @@ services:
environment:
- DB_PORT=3306
- DB_HOST=mysql
- DB_USERNAME=userName
- DB_USER=userName
- DB_PASSWORD=${DB_PASSWORD}
+3 -1
View File
@@ -27,7 +27,7 @@ spec:
value: ""
- name: DB_NAME
value: "dbname"
- name: DB_USERNAME
- name: DB_USER
value: "username"
# Please use secret!
- name: DB_PASSWORD
@@ -44,4 +44,6 @@ spec:
value: "xxxx"
- name: AWS_DISABLE_SSL
value: "false"
- name: AWS_FORCE_PATH_STYLE
value: "true"
restartPolicy: Never
+89 -12
View File
@@ -1,25 +1,102 @@
module github.com/jkaninda/mysql-bkup
go 1.22.5
require github.com/spf13/pflag v1.0.5
go 1.26.0
require (
github.com/aws/aws-sdk-go v1.55.3
github.com/bramvdbogaerde/go-scp v1.5.0
github.com/hpcloud/tail v1.0.0
github.com/spf13/cobra v1.8.0
golang.org/x/crypto v0.18.0
filippo.io/age v1.3.2
github.com/jkaninda/bkupkit v0.0.2
)
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/jkaninda/encryptor v0.3.0 // indirect
github.com/jkaninda/go-storage v0.2.0 // indirect
github.com/jkaninda/okapi v0.11.0 // indirect
github.com/jlaffaye/ftp v0.2.0 // indirect
github.com/jmespath/go-jmespath v0.4.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
golang.org/x/sys v0.22.0 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // 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
)
+233 -33
View File
@@ -1,47 +1,247 @@
github.com/aws/aws-sdk-go v1.55.3 h1:0B5hOX+mIx7I5XPOrjrHlKSDQV/+ypFZpIHOx5LOk3E=
github.com/aws/aws-sdk-go v1.55.3/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d h1:Blprhc2SbChNZtWcU+BLTM4YdoqYAS9V7cJgOwJKyAs=
c2sp.org/CCTV/age v0.0.0-20260829155415-4448f2097b2d/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs=
cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jkaninda/bkupkit v0.0.2 h1:3a5J/8qwXX7TWYBwIX57OQvUQCqo3er/JZZwvgU8I1g=
github.com/jkaninda/bkupkit v0.0.2/go.mod h1:TB2HVP8jRau/TdwfsO4QOgJY4Vi4h6HX+BlxJzXdcIg=
github.com/jkaninda/encryptor v0.3.0 h1:m7r7ROyjXtVXpFTtdGAhHdcPLDD+6kb0eaTF9cY4KM4=
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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.7.0 h1:uXe1MflJoHw58wAUvxVlcM7WpKtijWG7I1UidcGh6g4=
github.com/spiffe/go-spiffe/v2 v2.7.0/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw=
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/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+639
View File
@@ -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)
}
}
+279
View File
@@ -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
}
+199
View File
@@ -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
}
+487
View File
@@ -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
}
+407
View File
@@ -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")
}
}
+101
View File
@@ -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()
}
+14 -10
View File
@@ -1,15 +1,19 @@
// Package main /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
// 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
import "github.com/jkaninda/mysql-bkup/cmd"
import (
"github.com/jkaninda/bkupkit/cli"
"github.com/jkaninda/mysql-bkup/internal/mysql"
)
var Version = "development"
func main() {
cmd.Execute()
(&cli.App{
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()
}
+35
View File
@@ -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');
+13
View File
@@ -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
-298
View File
@@ -1,298 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
import (
"fmt"
"github.com/jkaninda/mysql-bkup/utils"
"github.com/robfig/cron/v3"
"github.com/spf13/cobra"
"log"
"os"
"os/exec"
"path/filepath"
"time"
)
func StartBackup(cmd *cobra.Command) {
intro()
dbConf = initDbConfig(cmd)
//Initialize backup configs
config := initBackupConfig(cmd)
if config.cronExpression == "" {
BackupTask(dbConf, config)
} else {
if utils.IsValidCronExpression(config.cronExpression) {
scheduledMode(dbConf, config)
} else {
utils.Fatal("Cron expression is not valid: %s", config.cronExpression)
}
}
}
// Run in scheduled mode
func scheduledMode(db *dbConfig, config *BackupConfig) {
utils.Info("Running in Scheduled mode")
utils.Info("Backup cron expression: %s", config.cronExpression)
utils.Info("Storage type %s ", config.storage)
//Test database connexion
testDatabaseConnection(db)
//Test backup
utils.Info("Testing backup configurations...")
BackupTask(db, config)
utils.Info("Testing backup configurations...done")
utils.Info("Creating backup job...")
// Create a new cron instance
c := cron.New()
_, err := c.AddFunc(config.cronExpression, func() {
BackupTask(db, config)
})
if err != nil {
return
}
// Start the cron scheduler
c.Start()
utils.Info("Creating backup job...done")
utils.Info("Backup job started")
defer c.Stop()
select {}
}
func BackupTask(db *dbConfig, config *BackupConfig) {
//Generate backup file name
backupFileName := fmt.Sprintf("%s_%s.sql.gz", db.dbName, time.Now().Format("20060102_150405"))
if config.disableCompression {
backupFileName = fmt.Sprintf("%s_%s.sql", db.dbName, time.Now().Format("20060102_150405"))
}
config.backupFileName = backupFileName
switch config.storage {
case "local":
localBackup(db, config)
case "s3":
s3Backup(db, config)
case "ssh", "remote":
sshBackup(db, config)
case "ftp":
ftpBackup(db, config)
default:
localBackup(db, config)
}
}
// BackupDatabase backup database
func BackupDatabase(db *dbConfig, backupFileName string, disableCompression bool) {
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...")
err = os.Setenv("MYSQL_PWD", db.dbPassword)
if err != nil {
return
}
testDatabaseConnection(db)
// Backup Database database
utils.Info("Backing up database...")
if disableCompression {
// Execute mysqldump
cmd := exec.Command("mysqldump",
"-h", db.dbHost,
"-P", db.dbPort,
"-u", db.dbUserName,
db.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", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, db.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(db *dbConfig, config *BackupConfig) {
utils.Info("Backup database to local storage")
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, gpgExtension)
}
utils.Info("Backup name is %s", finalFileName)
moveToBackup(finalFileName, storagePath)
//Send notification
utils.NotifySuccess(finalFileName)
//Delete old backup
if config.prune {
deleteOldBackup(config.backupRetention)
}
//Delete temp
deleteTemp()
}
func s3Backup(db *dbConfig, config *BackupConfig) {
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(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to remote storage S3 ... ")
utils.Info("Backup name is %s", finalFileName)
err := 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, config.backupFileName))
if err != nil {
fmt.Println("Error deleting file: ", err)
}
// Delete old backup
if config.prune {
err := DeleteOldBackup(bucket, s3Path, config.backupRetention)
if err != nil {
utils.Fatal("Error deleting old backup from S3: %s ", err)
}
}
utils.Done("Uploading backup archive to remote storage S3 ... done ")
//Send notification
utils.NotifySuccess(finalFileName)
//Delete temp
deleteTemp()
}
// sshBackup backup database to SSH remote server
func sshBackup(db *dbConfig, config *BackupConfig) {
utils.Info("Backup database to Remote server")
//Backup database
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to remote storage ... ")
utils.Info("Backup name is %s", finalFileName)
err := CopyToRemote(finalFileName, config.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 config.prune {
//TODO: Delete old backup from remote server
utils.Info("Deleting old backup from a remote server is not implemented yet")
}
utils.Done("Uploading backup archive to remote storage ... done ")
//Send notification
utils.NotifySuccess(finalFileName)
//Delete temp
deleteTemp()
}
func ftpBackup(db *dbConfig, config *BackupConfig) {
utils.Info("Backup database to the remote FTP server")
//Backup database
BackupDatabase(db, config.backupFileName, disableCompression)
finalFileName := config.backupFileName
if config.encryption {
encryptBackup(config.backupFileName, config.passphrase)
finalFileName = fmt.Sprintf("%s.%s", config.backupFileName, "gpg")
}
utils.Info("Uploading backup archive to the remote FTP server ... ")
utils.Info("Backup name is %s", finalFileName)
err := CopyToFTP(finalFileName, config.remotePath)
if err != nil {
utils.Fatal("Error uploading file to the remote FTP server: %s ", err)
}
//Delete backup file from tmp folder
err = utils.DeleteFile(filepath.Join(tmpPath, finalFileName))
if err != nil {
utils.Error("Error deleting file: %v", err)
}
if config.prune {
//TODO: Delete old backup from remote server
utils.Info("Deleting old backup from a remote server is not implemented yet")
}
utils.Done("Uploading backup archive to the remote FTP server ... done ")
//Send notification
utils.NotifySuccess(finalFileName)
//Delete temp
deleteTemp()
}
// encryptBackup encrypt backup
func encryptBackup(backupFileName, passphrase string) {
err := Encrypt(filepath.Join(tmpPath, backupFileName), passphrase)
if err != nil {
utils.Fatal("Error during encrypting backup %s", err)
}
}
-161
View File
@@ -1,161 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
import (
"github.com/jkaninda/mysql-bkup/utils"
"github.com/spf13/cobra"
"os"
)
type Config struct {
}
type dbConfig struct {
dbHost string
dbPort string
dbName string
dbUserName string
dbPassword string
}
type targetDbConfig struct {
targetDbHost string
targetDbPort string
targetDbUserName string
targetDbPassword string
targetDbName string
}
type BackupConfig struct {
backupFileName string
backupRetention int
disableCompression bool
prune bool
encryption bool
remotePath string
passphrase string
storage string
cronExpression string
}
type RestoreConfig struct {
s3Path string
remotePath string
storage string
file string
bucket string
gpqPassphrase string
}
type FTPConfig struct {
host string
user string
password string
port string
remotePath string
}
func initDbConfig(cmd *cobra.Command) *dbConfig {
//Set env
utils.GetEnv(cmd, "dbname", "DB_NAME")
dConf := dbConfig{}
dConf.dbHost = os.Getenv("DB_HOST")
dConf.dbPort = os.Getenv("DB_PORT")
dConf.dbName = os.Getenv("DB_NAME")
dConf.dbUserName = os.Getenv("DB_USERNAME")
dConf.dbPassword = os.Getenv("DB_PASSWORD")
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)
}
return &dConf
}
func initBackupConfig(cmd *cobra.Command) *BackupConfig {
utils.SetEnv("STORAGE_PATH", storagePath)
utils.GetEnv(cmd, "cron-expression", "BACKUP_CRON_EXPRESSION")
utils.GetEnv(cmd, "period", "BACKUP_CRON_EXPRESSION")
utils.GetEnv(cmd, "path", "REMOTE_PATH")
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
//Get flag value and set env
storage = utils.GetEnv(cmd, "storage", "STORAGE")
backupRetention, _ := cmd.Flags().GetInt("keep-last")
prune, _ := cmd.Flags().GetBool("prune")
disableCompression, _ = cmd.Flags().GetBool("disable-compression")
_, _ = cmd.Flags().GetString("mode")
passphrase := os.Getenv("GPG_PASSPHRASE")
_ = utils.GetEnv(cmd, "path", "AWS_S3_PATH")
cronExpression := os.Getenv("BACKUP_CRON_EXPRESSION")
if passphrase != "" {
encryption = true
}
//Initialize backup configs
config := BackupConfig{}
config.backupRetention = backupRetention
config.disableCompression = disableCompression
config.prune = prune
config.storage = storage
config.encryption = encryption
config.remotePath = remotePath
config.passphrase = passphrase
config.cronExpression = cronExpression
return &config
}
func initRestoreConfig(cmd *cobra.Command) *RestoreConfig {
utils.SetEnv("STORAGE_PATH", storagePath)
utils.GetEnv(cmd, "path", "REMOTE_PATH")
remotePath := utils.GetEnvVariable("REMOTE_PATH", "SSH_REMOTE_PATH")
//Get flag value and set env
s3Path := utils.GetEnv(cmd, "path", "AWS_S3_PATH")
storage = utils.GetEnv(cmd, "storage", "STORAGE")
file = utils.GetEnv(cmd, "file", "FILE_NAME")
_, _ = cmd.Flags().GetString("mode")
bucket := utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
gpqPassphrase := os.Getenv("GPG_PASSPHRASE")
//Initialize restore configs
rConfig := RestoreConfig{}
rConfig.s3Path = s3Path
rConfig.remotePath = remotePath
rConfig.storage = storage
rConfig.bucket = bucket
rConfig.file = file
rConfig.storage = storage
rConfig.gpqPassphrase = gpqPassphrase
return &rConfig
}
func initTargetDbConfig() *targetDbConfig {
tdbConfig := targetDbConfig{}
tdbConfig.targetDbHost = os.Getenv("TARGET_DB_HOST")
tdbConfig.targetDbPort = os.Getenv("TARGET_DB_PORT")
tdbConfig.targetDbName = os.Getenv("TARGET_DB_NAME")
tdbConfig.targetDbUserName = os.Getenv("TARGET_DB_USERNAME")
tdbConfig.targetDbPassword = os.Getenv("TARGET_DB_PASSWORD")
err := utils.CheckEnvVars(tdbRVars)
if err != nil {
utils.Error("Please make sure all required environment variables for the target database are set")
utils.Fatal("Error checking target database environment variables: %s", err)
}
return &tdbConfig
}
func initFtpConfig() *FTPConfig {
//Initialize backup configs
fConfig := FTPConfig{}
fConfig.host = os.Getenv("FTP_HOST_NAME")
fConfig.user = os.Getenv("FTP_USER")
fConfig.password = os.Getenv("FTP_PASSWORD")
fConfig.port = os.Getenv("FTP_PORT")
fConfig.remotePath = os.Getenv("REMOTE_PATH")
err := utils.CheckEnvVars(ftpVars)
if err != nil {
utils.Error("Please make sure all required environment variables for FTP are set")
utils.Fatal("Error checking environment variables: %s", err)
}
return &fConfig
}
-63
View File
@@ -1,63 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
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 + " ...")
//Create gpg home dir
err := utils.MakeDirAll(gpgHome)
if err != nil {
return err
}
utils.SetEnv("GNUPGHOME", gpgHome)
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...")
//Create gpg home dir
err := utils.MakeDirAll(gpgHome)
if err != nil {
return err
}
utils.SetEnv("GNUPGHOME", gpgHome)
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
}
-81
View File
@@ -1,81 +0,0 @@
package pkg
import (
"fmt"
"github.com/jlaffaye/ftp"
"io"
"os"
"path/filepath"
"time"
)
// initFtpClient initializes and authenticates an FTP client
func initFtpClient() (*ftp.ServerConn, error) {
ftpConfig := initFtpConfig()
ftpClient, err := ftp.Dial(fmt.Sprintf("%s:%s", ftpConfig.host, ftpConfig.port), ftp.DialWithTimeout(5*time.Second))
if err != nil {
return nil, fmt.Errorf("failed to connect to FTP: %w", err)
}
err = ftpClient.Login(ftpConfig.user, ftpConfig.password)
if err != nil {
return nil, fmt.Errorf("failed to log in to FTP: %w", err)
}
return ftpClient, nil
}
// CopyToFTP uploads a file to the remote FTP server
func CopyToFTP(fileName, remotePath string) (err error) {
ftpConfig := initFtpConfig()
ftpClient, err := initFtpClient()
if err != nil {
return err
}
defer ftpClient.Quit()
filePath := filepath.Join(tmpPath, fileName)
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", fileName, err)
}
defer file.Close()
remoteFilePath := filepath.Join(ftpConfig.remotePath, fileName)
err = ftpClient.Stor(remoteFilePath, file)
if err != nil {
return fmt.Errorf("failed to upload file %s: %w", fileName, err)
}
return nil
}
// CopyFromFTP downloads a file from the remote FTP server
func CopyFromFTP(fileName, remotePath string) (err error) {
ftpClient, err := initFtpClient()
if err != nil {
return err
}
defer ftpClient.Quit()
remoteFilePath := filepath.Join(remotePath, fileName)
r, err := ftpClient.Retr(remoteFilePath)
if err != nil {
return fmt.Errorf("failed to retrieve file %s: %w", fileName, err)
}
defer r.Close()
localFilePath := filepath.Join(tmpPath, fileName)
outFile, err := os.Create(localFilePath)
if err != nil {
return fmt.Errorf("failed to create local file %s: %w", fileName, err)
}
defer outFile.Close()
_, err = io.Copy(outFile, r)
if err != nil {
return fmt.Errorf("failed to copy data to local file %s: %w", fileName, err)
}
return nil
}
-131
View File
@@ -1,131 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
import (
"bytes"
"fmt"
"github.com/jkaninda/mysql-bkup/utils"
"os"
"os/exec"
"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
}
utils.Done("Deleting old backups...done")
}
func deleteTemp() {
utils.Info("Deleting %s ...", tmpPath)
err := filepath.Walk(tmpPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Check if the current item is a file
if !info.IsDir() {
// Delete the file
err = os.Remove(path)
if err != nil {
return err
}
}
return nil
})
if err != nil {
utils.Error("Error deleting files: %v", err)
} else {
utils.Info("Deleting %s ... done", tmpPath)
}
}
// TestDatabaseConnection tests the database connection
func testDatabaseConnection(db *dbConfig) {
err := os.Setenv("MYSQL_PWD", db.dbPassword)
if err != nil {
return
}
utils.Info("Connecting to %s database ...", db.dbName)
cmd := exec.Command("mysql", "-h", db.dbHost, "-P", db.dbPort, "-u", db.dbUserName, db.dbName, "-e", "quit")
// Capture the output
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
err = cmd.Run()
if err != nil {
utils.Fatal("Error testing database connection: %v\nOutput: %s", err, out.String())
}
utils.Info("Successfully connected to %s database", db.dbName)
}
func intro() {
utils.Info("Starting MySQL Backup...")
utils.Info("Copyright (c) 2024 Jonas Kaninda ")
}
-40
View File
@@ -1,40 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
import (
"fmt"
"github.com/jkaninda/mysql-bkup/utils"
"github.com/spf13/cobra"
"time"
)
func StartMigration(cmd *cobra.Command) {
intro()
utils.Info("Starting database migration...")
//Get DB config
dbConf = initDbConfig(cmd)
targetDbConf = initTargetDbConfig()
//Defining the target database variables
newDbConfig := dbConfig{}
newDbConfig.dbHost = targetDbConf.targetDbHost
newDbConfig.dbPort = targetDbConf.targetDbPort
newDbConfig.dbName = targetDbConf.targetDbName
newDbConfig.dbUserName = targetDbConf.targetDbUserName
newDbConfig.dbPassword = targetDbConf.targetDbPassword
//Generate file name
backupFileName := fmt.Sprintf("%s_%s.sql", dbConf.dbName, time.Now().Format("20060102_150405"))
//Backup source Database
BackupDatabase(dbConf, backupFileName, true)
//Restore source database into target database
utils.Info("Restoring [%s] database into [%s] database...", dbConf.dbName, targetDbConf.targetDbName)
RestoreDatabase(&newDbConfig, backupFileName)
utils.Info("[%s] database has been restored into [%s] database", dbConf.dbName, targetDbConf.targetDbName)
utils.Info("Database migration completed.")
}
-135
View File
@@ -1,135 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
import (
"fmt"
"github.com/jkaninda/mysql-bkup/utils"
"github.com/spf13/cobra"
"os"
"os/exec"
"path/filepath"
)
func StartRestore(cmd *cobra.Command) {
intro()
dbConf = initDbConfig(cmd)
restoreConf := initRestoreConfig(cmd)
switch restoreConf.storage {
case "s3":
restoreFromS3(dbConf, restoreConf.file, restoreConf.bucket, restoreConf.s3Path)
case "local":
utils.Info("Restore database from local")
copyToTmp(storagePath, restoreConf.file)
RestoreDatabase(dbConf, restoreConf.file)
case "ssh":
restoreFromRemote(dbConf, restoreConf.file, restoreConf.remotePath)
case "ftp":
restoreFromFTP(dbConf, restoreConf.file, restoreConf.remotePath)
default:
utils.Info("Restore database from local")
copyToTmp(storagePath, restoreConf.file)
RestoreDatabase(dbConf, restoreConf.file)
}
}
func restoreFromS3(db *dbConfig, file, bucket, s3Path string) {
utils.Info("Restore database from s3")
err := DownloadFile(tmpPath, file, bucket, s3Path)
if err != nil {
utils.Fatal("Error download file from s3 %s %v", file, err)
}
RestoreDatabase(db, file)
}
func restoreFromRemote(db *dbConfig, file, remotePath string) {
utils.Info("Restore database from remote server")
err := CopyFromRemote(file, remotePath)
if err != nil {
utils.Fatal("Error download file from remote server: %s %v ", filepath.Join(remotePath, file), err)
}
RestoreDatabase(db, file)
}
func restoreFromFTP(db *dbConfig, file, remotePath string) {
utils.Info("Restore database from FTP server")
err := CopyFromFTP(file, remotePath)
if err != nil {
utils.Fatal("Error download file from FTP server: %s %v", filepath.Join(remotePath, file), err)
}
RestoreDatabase(db, file)
}
// RestoreDatabase restore database
func RestoreDatabase(db *dbConfig, file string) {
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)) {
err = os.Setenv("MYSQL_PWD", db.dbPassword)
if err != nil {
return
}
testDatabaseConnection(db)
utils.Info("Restoring database...")
extension := filepath.Ext(fmt.Sprintf("%s/%s", tmpPath, file))
// Restore from compressed file / .sql.gz
if extension == ".gz" {
str := "zcat " + filepath.Join(tmpPath, file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
_, err := exec.Command("sh", "-c", str).Output()
if err != nil {
utils.Fatal("Error, in restoring the database %v", err)
}
utils.Info("Restoring database... done")
utils.Done("Database has been restored")
//Delete temp
deleteTemp()
} else if extension == ".sql" {
//Restore from sql file
str := "cat " + filepath.Join(tmpPath, file) + " | mysql -h " + db.dbHost + " -P " + db.dbPort + " -u " + db.dbUserName + " " + db.dbName
_, err := exec.Command("sh", "-c", str).Output()
if err != nil {
utils.Fatal("Error in restoring the database %v", err)
}
utils.Info("Restoring database... done")
utils.Done("Database has been restored")
//Delete temp
deleteTemp()
} else {
utils.Fatal("Unknown file extension %s", extension)
}
} else {
utils.Fatal("File not found in %s", filepath.Join(tmpPath, file))
}
}
-176
View File
@@ -1,176 +0,0 @@
// Package utils /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
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"
"github.com/jkaninda/mysql-bkup/utils"
"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 := utils.GetEnvVariable("AWS_S3_ENDPOINT", "S3_ENDPOINT")
accessKey := utils.GetEnvVariable("AWS_ACCESS_KEY", "ACCESS_KEY")
secretKey := utils.GetEnvVariable("AWS_SECRET_KEY", "SECRET_KEY")
_ = utils.GetEnvVariable("AWS_S3_BUCKET_NAME", "BUCKET_NAME")
region := os.Getenv("AWS_REGION")
awsDisableSsl, err := strconv.ParseBool(os.Getenv("AWS_DISABLE_SSL"))
if err != nil {
utils.Fatal("Unable to parse AWS_DISABLE_SSL env var: %s", err)
}
err = utils.CheckEnvVars(awsVars)
if err != nil {
utils.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
}
utils.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 {
utils.Error("Failed to download file %s", key)
return err
}
utils.Info("Backup downloaded: %s bytes size %s ", file.Name(), 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
}
-121
View File
@@ -1,121 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
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
}
-53
View File
@@ -1,53 +0,0 @@
// Package pkg /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package pkg
const cronLogFile = "/var/log/mysql-bkup.log"
const tmpPath = "/tmp/backup"
const algorithm = "aes256"
const gpgHome = "/config/gnupg"
const gpgExtension = "gpg"
var (
storage = "local"
file = ""
storagePath = "/backup"
disableCompression = false
encryption = false
)
// dbHVars Required environment variables for database
var dbHVars = []string{
"DB_HOST",
"DB_PASSWORD",
"DB_USERNAME",
"DB_NAME",
}
var tdbRVars = []string{
"TARGET_DB_HOST",
"TARGET_DB_PORT",
"TARGET_DB_NAME",
"TARGET_DB_USERNAME",
"TARGET_DB_PASSWORD",
}
var dbConf *dbConfig
var targetDbConf *targetDbConfig
// sshHVars Required environment variables for SSH remote server storage
var sshHVars = []string{
"SSH_USER",
"REMOTE_PATH",
"SSH_HOST_NAME",
"SSH_PORT",
}
var ftpVars = []string{
"FTP_HOST_NAME",
"FTP_USER",
"FTP_PASSWORD",
"FTP_PORT",
}
-16
View File
@@ -1,16 +0,0 @@
// Package utils /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package utils
const RestoreExample = "mysql-bkup restore --dbname database --file db_20231219_022941.sql.gz\n" +
"restore --dbname database --storage s3 --path /custom-path --file db_20231219_022941.sql.gz"
const BackupExample = "mysql-bkup backup --dbname database --disable-compression\n" +
"backup --dbname database --storage s3 --path /custom-path --disable-compression"
const MainExample = "mysql-bkup backup --dbname database --disable-compression\n" +
"backup --dbname database --storage s3 --path /custom-path\n" +
"restore --dbname database --file db_20231219_022941.sql.gz"
-67
View File
@@ -1,67 +0,0 @@
// Package utils /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
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)
NotifyError(msg)
} else {
fmt.Printf("%s ERROR: %s\n", currentTime, formattedMessage)
NotifyError(formattedMessage)
}
os.Exit(1)
os.Kill.Signal()
}
-257
View File
@@ -1,257 +0,0 @@
// Package utils /
/*****
@author Jonas Kaninda
@license MIT License <https://opensource.org/licenses/MIT>
@Copyright © 2024 Jonas Kaninda
**/
package utils
import (
"bytes"
"encoding/json"
"fmt"
"github.com/robfig/cron/v3"
"github.com/spf13/cobra"
"io"
"io/fs"
"io/ioutil"
"net/http"
"os"
"strconv"
)
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
}
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
}
// MakeDir create directory
func MakeDir(dirPath string) error {
err := os.Mkdir(dirPath, 0700)
if err != nil {
return err
}
return nil
}
// MakeDirAll create directory
func MakeDirAll(dirPath string) error {
err := os.MkdirAll(dirPath, 0700)
if err != nil {
return err
}
return nil
}
func GetIntEnv(envName string) int {
val := os.Getenv(envName)
if val == "" {
return 0
}
ret, err := strconv.Atoi(val)
if err != nil {
Error("Error: %v", err)
}
return ret
}
func sendMessage(msg string) {
Info("Sending notification... ")
chatId := os.Getenv("TG_CHAT_ID")
body, _ := json.Marshal(map[string]string{
"chat_id": chatId,
"text": msg,
})
url := fmt.Sprintf("%s/sendMessage", getTgUrl())
// Create an HTTP post request
request, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
panic(err)
}
request.Header.Add("Content-Type", "application/json")
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
panic(err)
}
code := response.StatusCode
if code == 200 {
Info("Notification has been sent")
} else {
body, _ := ioutil.ReadAll(response.Body)
Error("Message not sent, error: %s", string(body))
}
}
func NotifySuccess(fileName string) {
var vars = []string{
"TG_TOKEN",
"TG_CHAT_ID",
}
//Telegram notification
err := CheckEnvVars(vars)
if err == nil {
message := "[✅ MySQL Backup ]\n" +
"Database has been backed up \n" +
"Backup name is " + fileName
sendMessage(message)
}
}
func NotifyError(error string) {
var vars = []string{
"TG_TOKEN",
"TG_CHAT_ID",
}
//Telegram notification
err := CheckEnvVars(vars)
if err == nil {
message := "[🔴 MySQL Backup ]\n" +
"An error occurred during database backup \n" +
"Error: " + error
sendMessage(message)
}
}
func getTgUrl() string {
return fmt.Sprintf("https://api.telegram.org/bot%s", os.Getenv("TG_TOKEN"))
}
func IsValidCronExpression(cronExpr string) bool {
_, err := cron.ParseStandard(cronExpr)
return err == nil
}