diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..5c3d1eb --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,292 @@ +name: Tests +on: + push: + pull_request: +#on: +# push: +# branches: +# - main +# pull_request: +# branches: +# - main + +env: + IMAGE_NAME: mysql-bkup + +jobs: + test: + runs-on: ubuntu-latest + services: + mysql: + image: mysql:9 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: testdb + MYSQL_USER: user + MYSQL_PASSWORD: password + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -uuser -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + mysql8: + image: mysql:8 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: testdb + MYSQL_USER: user + MYSQL_PASSWORD: password + ports: + - 3308:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -uuser -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + mysql5: + image: mysql:5 + env: + MYSQL_ROOT_PASSWORD: password + MYSQL_DATABASE: testdb + MYSQL_USER: user + MYSQL_PASSWORD: password + ports: + - 3305:3306 + options: >- + --health-cmd="mysqladmin ping -h 127.0.0.1 -uuser -ppassword" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Create Minio container + run: | + docker run -d --rm --name minio \ + --network host \ + -p 9000:9000 \ + -e MINIO_ACCESS_KEY=minioadmin \ + -e MINIO_SECRET_KEY=minioadmin \ + -e MINIO_REGION_NAME="eu" \ + minio/minio server /data + echo "Create Minio container completed" + - name: Install MinIO Client (mc) + run: | + curl -O https://dl.min.io/client/mc/release/linux-amd64/mc + chmod +x mc + sudo mv mc /usr/local/bin/ + + - name: Wait for MinIO to be ready + run: sleep 5 + + - name: Configure MinIO Client + run: | + mc alias set local http://localhost:9000 minioadmin minioadmin + mc alias list + + - name: Create MinIO Bucket + run: | + mc mb local/backups + echo "Bucket backups created successfully." + # Build the Docker image + - name: Build Docker Image + run: | + docker buildx build --build-arg appVersion=test -t ${{ env.IMAGE_NAME }}:latest --load . + + - name: Verify Docker images + run: | + docker images + + - name: Wait for MySQL to be ready + run: | + docker run --rm --network host mysql:9 mysqladmin ping -h 127.0.0.1 -uuser -ppassword --wait + - name: Test restore + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=root \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest restore -f init.sql + echo "Database restore completed" + - name: Test restore Mysql8 + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_PORT=3308 \ + -e DB_USERNAME=root \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest restore -f init.sql + echo "Test restore Mysql8 completed" + - name: Test restore Mysql5 + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_PORT=3305 \ + -e DB_USERNAME=root \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest restore -f init.sql + echo "Test restore Mysql5 completed" + - name: Test backup + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest backup + echo "Database backup completed" + - name: Test backup Mysql8 + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_PORT=3308 \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest backup + echo "Test backup Mysql8 completed" + - name: Test backup Mysql5 + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_PORT=3305 \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest backup + echo "Test backup Mysql5 completed" + - name: Test encrypted backup + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e GPG_PASSPHRASE=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest backup --disable-compression --custom-name encrypted-bkup + echo "Database encrypted backup completed" + - name: Test restore encrypted backup | testdb -> testdb2 + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=root \ + -e DB_PASSWORD=password \ + -e GPG_PASSPHRASE=password \ + -e DB_NAME=testdb2 \ + ${{ env.IMAGE_NAME }}:latest restore -f /backup/encrypted-bkup.sql.gpg + echo "Test restore encrypted backup completed" + - name: Test migrate database testdb -> testdb3 + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=root \ + -e DB_PASSWORD=password \ + -e GPG_PASSPHRASE=password \ + -e DB_NAME=testdb \ + -e TARGET_DB_HOST=127.0.0.1 \ + -e TARGET_DB_PORT=3306 \ + -e TARGET_DB_NAME=testdb3 \ + -e TARGET_DB_USERNAME=root \ + -e TARGET_DB_PASSWORD=password \ + ${{ env.IMAGE_NAME }}:latest migrate + echo "Test migrate database testdb -> testdb3 completed" + - name: Test backup all databases + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=root \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest backup --all-databases + echo "Database backup completed" + - name: Test multiple backup + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e TESTDB2_DB_USERNAME=root \ + -e TESTDB2_DB_PASSWORD=password \ + -e TESTDB2_DB_HOST=127.0.0.1 \ + ${{ env.IMAGE_NAME }}:latest backup -c /backup/test_config.yaml + echo "Database backup completed" + - name: Test backup Minio (s3) + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + -e AWS_S3_ENDPOINT="http://127.0.0.1:9000" \ + -e AWS_S3_BUCKET_NAME=backups \ + -e AWS_ACCESS_KEY=minioadmin \ + -e AWS_SECRET_KEY=minioadmin \ + -e AWS_DISABLE_SSL="true" \ + -e AWS_REGION="eu" \ + -e AWS_FORCE_PATH_STYLE="true" ${{ env.IMAGE_NAME }}:latest backup -s s3 --custom-name minio-backup + echo "Test backup Minio (s3) completed" + - name: Test restore Minio (s3) + run: | + docker run --rm --name ${{ env.IMAGE_NAME }} \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + -e AWS_S3_ENDPOINT="http://127.0.0.1:9000" \ + -e AWS_S3_BUCKET_NAME=backups \ + -e AWS_ACCESS_KEY=minioadmin \ + -e AWS_SECRET_KEY=minioadmin \ + -e AWS_DISABLE_SSL="true" \ + -e AWS_REGION="eu" \ + -e AWS_FORCE_PATH_STYLE="true" ${{ env.IMAGE_NAME }}:latest restore -s s3 -f minio-backup.sql.gz + echo "Test backup Minio (s3) completed" + - name: Test scheduled backup + run: | + docker run -d --rm --name ${{ env.IMAGE_NAME }} \ + -v ./migrations:/backup/ \ + --network host \ + -e DB_HOST=127.0.0.1 \ + -e DB_USERNAME=user \ + -e DB_PASSWORD=password \ + -e DB_NAME=testdb \ + ${{ env.IMAGE_NAME }}:latest backup -e "@every 10s" + + echo "Waiting for backup to be done..." + sleep 25 + docker logs ${{ env.IMAGE_NAME }} + echo "Test scheduled backup completed" + # Cleanup: Stop and remove containers + - name: Clean up + run: | + docker stop ${{ env.IMAGE_NAME }} || true + docker rm ${{ env.IMAGE_NAME }} || true \ No newline at end of file diff --git a/README.md b/README.md index f827a93..2c9500a 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ **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. +[![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) diff --git a/cmd/backup.go b/cmd/backup.go index 4873ceb..290ad2a 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -52,5 +52,6 @@ func init() { BackupCmd.PersistentFlags().BoolP("disable-compression", "", false, "Disable backup compression") BackupCmd.PersistentFlags().BoolP("all-databases", "a", false, "Backup all databases") BackupCmd.PersistentFlags().BoolP("all-in-one", "A", false, "Backup all databases in a single file") + BackupCmd.PersistentFlags().StringP("custom-name", "", "", "Custom backup name") } diff --git a/migrations/init.sql b/migrations/init.sql new file mode 100644 index 0000000..11eb42e --- /dev/null +++ b/migrations/init.sql @@ -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'); diff --git a/migrations/test_config.yaml b/migrations/test_config.yaml new file mode 100644 index 0000000..97a8b56 --- /dev/null +++ b/migrations/test_config.yaml @@ -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_USERNAME + #TESTDB2_DB_PASSWORD + #TESTDB2_DB_HOST \ No newline at end of file diff --git a/pkg/backup.go b/pkg/backup.go index e4009ff..3c45164 100644 --- a/pkg/backup.go +++ b/pkg/backup.go @@ -51,6 +51,7 @@ func StartBackup(cmd *cobra.Command) { if err != nil { dbConf = initDbConfig(cmd) if config.cronExpression == "" { + config.allowCustomName = true createBackupTask(dbConf, config) } else { if utils.IsValidCronExpression(config.cronExpression) { @@ -145,11 +146,18 @@ func backupTask(db *dbConfig, config *BackupConfig) { if config.all && config.allInOne { prefix = "all_databases" } + // Generate file name backupFileName := fmt.Sprintf("%s_%s.sql.gz", prefix, time.Now().Format("20060102_150405")) if config.disableCompression { backupFileName = fmt.Sprintf("%s_%s.sql", prefix, time.Now().Format("20060102_150405")) } + if config.customName != "" && config.allowCustomName && !config.all { + backupFileName = fmt.Sprintf("%s.sql.gz", config.customName) + if config.disableCompression { + backupFileName = fmt.Sprintf("%s.sql", config.customName) + } + } config.backupFileName = backupFileName s := strings.ToLower(config.storage) switch s { diff --git a/pkg/config.go b/pkg/config.go index e02532e..19e8f3d 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -79,6 +79,8 @@ type BackupConfig struct { cronExpression string all bool allInOne bool + customName string + allowCustomName bool } type FTPConfig struct { host string @@ -259,6 +261,7 @@ func initBackupConfig(cmd *cobra.Command) *BackupConfig { prune = true } disableCompression, _ = cmd.Flags().GetBool("disable-compression") + customName, _ := cmd.Flags().GetString("custom-name") all, _ := cmd.Flags().GetBool("all-databases") allInOne, _ := cmd.Flags().GetBool("all-in-one") if allInOne { @@ -295,6 +298,7 @@ func initBackupConfig(cmd *cobra.Command) *BackupConfig { config.cronExpression = cronExpression config.all = all config.allInOne = allInOne + config.customName = customName return &config }