mirror of
https://github.com/Portabase/agent.git
synced 2026-09-11 02:27:10 +00:00
Compare commits
147 Commits
1.0.4-rc.14
...
1.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
| a1e18754b1 | |||
| 725312e1d5 | |||
| 8e8ebb5920 | |||
| 8489a6f6e3 | |||
| 3f5b9c91cc | |||
| 1c52f298d2 | |||
| e602c3540f | |||
| b5e8ccac1f | |||
| f14a6dc318 | |||
| e1bc77e481 | |||
| cbc7517603 | |||
| fc4e4e3881 | |||
| 5ec7bc9f28 | |||
| 45c33dd031 | |||
| cddb9d78fa | |||
| cf5de3115c | |||
| d6d2ef18f8 | |||
| 987c630c73 | |||
| e9147d6c60 | |||
| 174ead72a3 | |||
| f2a67b3490 | |||
| d4afefbc1e | |||
| be6e147df2 | |||
| 91b6658852 | |||
| f7dcb6b2bb | |||
| ab14f0dcf0 | |||
| 58a0b92710 | |||
| 2f24291c0e | |||
| 1fdea31430 | |||
| d1d633fd00 | |||
| b1c14f098b | |||
| 3abb955e24 | |||
| 6d22be9b50 | |||
| c2df4ba71e | |||
| 46b3f2466a | |||
| a2be03751f | |||
| 7df9df1605 | |||
| df5b16a153 | |||
| 824a5d52a3 | |||
| 1496208db4 | |||
| 454e1b442f | |||
| 54bca7452a | |||
| a96cef6cb5 | |||
| fd9f183166 | |||
| 94fb2535ec | |||
| 189ad866de | |||
| 55bbd4e724 | |||
| 7fabc9c98c | |||
| 4f67a57681 | |||
| 45a1118f64 | |||
| 063b2e1c2c | |||
| f2275d5aca | |||
| b136140b55 | |||
| 924177a4fe | |||
| 773077f66b | |||
| 46ff086cf9 | |||
| 678df8e2bc | |||
| f563c187db | |||
| df0dffa47b | |||
| 864396422b | |||
| e280c1d2f8 | |||
| 91a9d0654b | |||
| bc857a4c18 | |||
| 2454915866 | |||
| a4a7a9a2c4 | |||
| 997ba2afa2 | |||
| 7e0c7c0688 | |||
| f973bc1f52 | |||
| b9b9fcf6ee | |||
| 0224605c59 | |||
| 6ea9fdf8bc | |||
| ad68be8a5a | |||
| 434c131afb | |||
| 675ac5976d | |||
| 70543fe032 | |||
| 77f90308b4 | |||
| 63cb254abd | |||
| 7738745def | |||
| 82659f681c | |||
| eee3bebd50 | |||
| 58dd0259f9 | |||
| 5b7453208b | |||
| 35933aae52 | |||
| 178628cb1c | |||
| 3fc60e3f40 | |||
| e62de95827 | |||
| fc3612fa3f | |||
| 18fe211ad1 | |||
| 74ada01232 | |||
| 4fef360c06 | |||
| 2647941008 | |||
| b4bdc89888 | |||
| 829e5bade3 | |||
| fc9a4ef8fb | |||
| 3c5734ed4e | |||
| 4b77896ecb | |||
| 40c4a4e2bb | |||
| 8a05b93ff3 | |||
| c186932a79 | |||
| bf74173cac | |||
| 3dcebf4fcc | |||
| 4de2b81b33 | |||
| b06f9d8186 | |||
| f5a2182723 | |||
| e8a96980f1 | |||
| a64792bcc6 | |||
| 2d4542be5f | |||
| 4a00ed574d | |||
| fd220598fc | |||
| d8b239a123 | |||
| ebf0526366 | |||
| 6461c1523b | |||
| 2809373de1 | |||
| 5349e1fef9 | |||
| 2bcb393d60 | |||
| 406273796e | |||
| cb35347cb6 | |||
| b92138fd84 | |||
| f4c54836f1 | |||
| e678cf4b06 | |||
| 3c599e94de | |||
| 6bbc856f62 | |||
| 417212c9e8 | |||
| 552f1b8813 | |||
| e2d7c4747e | |||
| 08b5124a5a | |||
| cc0de33a62 | |||
| ce00959feb | |||
| cfef9b047b | |||
| caf6127f7a | |||
| b71f29eb21 | |||
| 2988cb297b | |||
| 5019e9585b | |||
| 027751febb | |||
| f6f378d584 | |||
| 66c0b04759 | |||
| c348253015 | |||
| e83e44a39e | |||
| 92d64c0b4b | |||
| abd39e2db1 | |||
| 48a4ef6cc2 | |||
| 358bffd675 | |||
| 25e08a2b94 | |||
| dc563d3cfd | |||
| 92ad1cbfb4 | |||
| 82d5306dd4 | |||
| 52714cda21 |
@@ -0,0 +1,23 @@
|
||||
reviews:
|
||||
auto_review:
|
||||
enabled: true
|
||||
ignore_usernames: ["dependabot[bot]"]
|
||||
labels: ["!wip", "!draft"]
|
||||
auto_apply_labels: true
|
||||
suggested_reviewers: true
|
||||
auto_assign_reviewers: true
|
||||
|
||||
issue_enrichment:
|
||||
auto_enrich:
|
||||
enabled: true
|
||||
labeling:
|
||||
auto_apply_labels: true
|
||||
labeling_instructions:
|
||||
- label: bug
|
||||
instructions: "Error, crash, or incorrect behavior reports"
|
||||
- label: enhancement
|
||||
instructions: "Feature requests or improvements"
|
||||
- label: documentation
|
||||
instructions: "Docs updates or missing documentation"
|
||||
- label: good first issue
|
||||
instructions: "Beginner-friendly tasks suitable for new contributors"
|
||||
@@ -0,0 +1,172 @@
|
||||
#name: Codecov Rust
|
||||
#
|
||||
#on:
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
# pull_request:
|
||||
# branches: ["main"]
|
||||
#
|
||||
#env:
|
||||
# CARGO_TERM_COLOR: always
|
||||
#
|
||||
#jobs:
|
||||
# coverage:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
#
|
||||
# - name: Install Rust toolchain
|
||||
# uses: dtolnay/rust-toolchain@stable
|
||||
# with:
|
||||
# components: llvm-tools-preview
|
||||
#
|
||||
# - name: Build test image (with grcov included)
|
||||
# run: docker compose -f docker-compose.test.yml build agent-test
|
||||
#
|
||||
# - name: Run tests in container
|
||||
# env:
|
||||
# CARGO_TARGET_DIR: /app/target
|
||||
# CARGO_INCREMENTAL: 0
|
||||
# RUSTFLAGS: "-C instrument-coverage -C link-dead-code"
|
||||
# LLVM_PROFILE_FILE: "/app/coverage/cargo-test-%p-%m.profraw"
|
||||
# run: |
|
||||
# docker compose -f docker-compose.test.yml run \
|
||||
# -e CARGO_TARGET_DIR \
|
||||
# -e CARGO_INCREMENTAL \
|
||||
# -e RUSTFLAGS \
|
||||
# -e LLVM_PROFILE_FILE \
|
||||
# agent-test bash -c "cargo clean && cargo test --verbose && sync"
|
||||
#
|
||||
# - name: Verify profraw files exist
|
||||
# run: |
|
||||
# docker compose -f docker-compose.test.yml run agent-test \
|
||||
# find /app/coverage -type f -name "*.profraw" | wc -l || true
|
||||
#
|
||||
# - name: Generate coverage report inside container
|
||||
# run: |
|
||||
# docker compose -f docker-compose.test.yml run agent-test bash -c "
|
||||
# rustup component add llvm-tools &&
|
||||
# grcov /app/coverage \
|
||||
# --binary-path /app/target/debug \
|
||||
# -s /app \
|
||||
# --llvm \
|
||||
# -t lcov \
|
||||
# --branch \
|
||||
# --ignore-not-existing \
|
||||
# --ignore '/app/target/*' \
|
||||
# --ignore '/*' \
|
||||
# -o /app/lcov.info
|
||||
# "
|
||||
#
|
||||
# - name: Copy lcov.info from container to host
|
||||
# run: |
|
||||
# docker compose -f docker-compose.test.yml cp agent-test:/app/lcov.info ./lcov.info
|
||||
#
|
||||
# - name: Remove container
|
||||
# run: |
|
||||
# docker rm agent-test-run
|
||||
#
|
||||
# - name: Show basic coverage report info (debug)
|
||||
# run: |
|
||||
# echo "lcov.info size:" $(wc -c ./lcov.info | awk '{print $1}')
|
||||
# head -n 30 ./lcov.info || true
|
||||
#
|
||||
# - name: Upload coverage to Codecov
|
||||
# uses: codecov/codecov-action@v5
|
||||
# with:
|
||||
# files: ./lcov.info
|
||||
# flags: unittests
|
||||
# name: rust-unit-coverage
|
||||
# verbose: true
|
||||
# fail_ci_if_error: true
|
||||
# env:
|
||||
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
name: Codecov Rust
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_TARGET_DIR: /app/target
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUSTFLAGS: "-C instrument-coverage -C link-dead-code"
|
||||
LLVM_PROFILE_FILE: "/app/coverage/cargo-test-%p-%m.profraw"
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
|
||||
- name: Build test image (with grcov included)
|
||||
run: docker compose -f docker-compose.test.yml build agent-test
|
||||
|
||||
- name: Start agent-test container
|
||||
run: docker compose -f docker-compose.test.yml up -d agent-test
|
||||
|
||||
- name: Run tests inside container
|
||||
run: |
|
||||
docker compose -f docker-compose.test.yml exec \
|
||||
-e CARGO_TARGET_DIR \
|
||||
-e CARGO_INCREMENTAL \
|
||||
-e RUSTFLAGS \
|
||||
-e LLVM_PROFILE_FILE \
|
||||
agent-test bash -c "
|
||||
cargo clean &&
|
||||
cargo test --verbose &&
|
||||
sync
|
||||
"
|
||||
|
||||
- name: Verify profraw files exist
|
||||
run: |
|
||||
docker compose -f docker-compose.test.yml exec \
|
||||
-e LLVM_PROFILE_FILE \
|
||||
agent-test find /app/coverage -type f -name "*.profraw" | wc -l || true
|
||||
|
||||
- name: Generate coverage report inside container
|
||||
run: |
|
||||
docker compose -f docker-compose.test.yml exec \
|
||||
agent-test bash -c "
|
||||
rustup component add llvm-tools &&
|
||||
grcov /app/coverage \
|
||||
--binary-path /app/target/debug \
|
||||
-s /app \
|
||||
--llvm \
|
||||
-t lcov \
|
||||
--branch \
|
||||
--ignore-not-existing \
|
||||
--ignore '/app/target/*' \
|
||||
--ignore '/*' \
|
||||
-o /app/lcov.info
|
||||
"
|
||||
|
||||
- name: Copy lcov.info from container to host
|
||||
run: docker compose -f docker-compose.test.yml cp agent-test:/app/lcov.info ./lcov.info
|
||||
|
||||
- name: Show basic coverage report info (debug)
|
||||
run: |
|
||||
echo "lcov.info size:" $(wc -c ./lcov.info | awk '{print $1}')
|
||||
head -n 30 ./lcov.info || true
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./lcov.info
|
||||
flags: unittests
|
||||
name: rust-unit-coverage
|
||||
verbose: true
|
||||
fail_ci_if_error: true
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Stop and remove container
|
||||
run: docker compose -f docker-compose.test.yml down
|
||||
@@ -0,0 +1,73 @@
|
||||
name: Discord Notification
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
release_tag:
|
||||
required: true
|
||||
type: string
|
||||
discord_title:
|
||||
required: true
|
||||
type: string
|
||||
discord_color:
|
||||
required: true
|
||||
type: number
|
||||
discord_footer:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
DISCORD_WEBHOOK:
|
||||
required: true
|
||||
GH_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
notify-discord:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send Discord Notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
RELEASE_INFO=$(gh release view "${{ inputs.release_tag }}" -R ${{ github.repository }} --json name,url,body,author)
|
||||
|
||||
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
|
||||
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ inputs.release_tag }}"; fi
|
||||
|
||||
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
|
||||
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
|
||||
|
||||
AUTHOR_NAME="Portabase"
|
||||
AUTHOR_ICON="https://github.com/Portabase.png"
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$RELEASE_TITLE" \
|
||||
--arg description "$RELEASE_BODY" \
|
||||
--arg url "$RELEASE_URL" \
|
||||
--arg author "$AUTHOR_NAME" \
|
||||
--arg icon "$AUTHOR_ICON" \
|
||||
--arg discord_title "${{ inputs.discord_title }}" \
|
||||
--arg discord_footer "${{ inputs.discord_footer }}" \
|
||||
--argjson discord_color ${{ inputs.discord_color }} \
|
||||
'{
|
||||
content: $discord_title,
|
||||
embeds: [{
|
||||
title: $title,
|
||||
url: $url,
|
||||
description: $description,
|
||||
color: $discord_color,
|
||||
author: {
|
||||
name: $author,
|
||||
icon_url: $icon
|
||||
},
|
||||
footer: {
|
||||
text: $discord_footer
|
||||
}
|
||||
}]
|
||||
}'
|
||||
)
|
||||
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DISCORD_WEBHOOK"
|
||||
+84
-143
@@ -1,83 +1,18 @@
|
||||
#name: Docker Publish
|
||||
#
|
||||
#on:
|
||||
# workflow_call:
|
||||
# inputs:
|
||||
# image_name:
|
||||
# required: false
|
||||
# type: string
|
||||
# default: 'portabase/agent'
|
||||
# add_latest:
|
||||
# required: false
|
||||
# type: boolean
|
||||
# default: false
|
||||
# target:
|
||||
# required: false
|
||||
# type: string
|
||||
# default: 'prod'
|
||||
# dockerfile:
|
||||
# required: false
|
||||
# type: string
|
||||
# default: './docker/Dockerfile'
|
||||
# secrets:
|
||||
# DOCKER_USERNAME:
|
||||
# required: true
|
||||
# DOCKER_PASSWORD:
|
||||
# required: true
|
||||
#
|
||||
#jobs:
|
||||
# build-and-push:
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# packages: write
|
||||
# contents: read
|
||||
# steps:
|
||||
# - name: Check out the repo
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
#
|
||||
# - name: Set up QEMU (enables multi-arch emulation)
|
||||
# uses: docker/setup-qemu-action@v3
|
||||
#
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@v3
|
||||
#
|
||||
# - name: Log in to Docker Hub
|
||||
# uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKER_USERNAME }}
|
||||
# password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
#
|
||||
# - name: Set tags
|
||||
# id: set-tags
|
||||
# run: |
|
||||
# REF_NAME=${GITHUB_REF#refs/tags/}
|
||||
# TAGS="${{ inputs.image_name }}:$REF_NAME"
|
||||
# if [[ "${{ inputs.add_latest }}" == "true" ]]; then
|
||||
# TAGS="$TAGS,${{ inputs.image_name }}:latest"
|
||||
# fi
|
||||
# echo "tags=$TAGS" >> $GITHUB_OUTPUT
|
||||
#
|
||||
# - name: Build and push Docker image
|
||||
# uses: docker/build-push-action@v6
|
||||
# with:
|
||||
# context: .
|
||||
# file: ${{ inputs.dockerfile }}
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
# push: true
|
||||
# tags: ${{ steps.set-tags.outputs.tags }}
|
||||
# target: ${{ inputs.target }}
|
||||
|
||||
name: Docker Publish
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
ref:
|
||||
required: true
|
||||
type: string
|
||||
image_name:
|
||||
required: false
|
||||
type: string
|
||||
default: 'portabase/agent'
|
||||
default: "portabase/agent"
|
||||
add_latest:
|
||||
required: false
|
||||
type: boolean
|
||||
@@ -85,11 +20,11 @@ on:
|
||||
target:
|
||||
required: false
|
||||
type: string
|
||||
default: 'prod'
|
||||
default: "prod"
|
||||
dockerfile:
|
||||
required: false
|
||||
type: string
|
||||
default: './docker/Dockerfile'
|
||||
default: "./docker/Dockerfile"
|
||||
secrets:
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
@@ -97,102 +32,108 @@ on:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-amd64:
|
||||
name: Build AMD64 Image
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image: ${{ steps.set-tags.outputs.amd64_image }}
|
||||
build:
|
||||
name: Build architectures
|
||||
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ linux/amd64, linux/arm64 ]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set image tag
|
||||
id: set-tags
|
||||
- name: Prepare Image Tags
|
||||
id: prep
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||
AMD64_IMAGE="${{ inputs.image_name }}:$REF_NAME-amd64"
|
||||
echo "amd64_image=$AMD64_IMAGE" >> $GITHUB_OUTPUT
|
||||
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
|
||||
echo "image=${{ inputs.image_name }}:${{inputs.version}}-$ARCH" >> $GITHUB_OUTPUT
|
||||
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push AMD64
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/amd64
|
||||
platforms: ${{ matrix.platform }}
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.amd64_image }}
|
||||
tags: ${{ steps.prep.outputs.image }}
|
||||
target: ${{ inputs.target }}
|
||||
|
||||
- name: Set job output
|
||||
run: |
|
||||
echo "image=${{ steps.set-tags.outputs.amd64_image }}" >> $GITHUB_OUTPUT
|
||||
- name: Save image name for manifest
|
||||
run: echo "${{ steps.prep.outputs.image }}" > image.txt
|
||||
|
||||
build-arm64:
|
||||
name: Build ARM64 Image
|
||||
runs-on: ubuntu-24.04-arm
|
||||
outputs:
|
||||
image: ${{ steps.set-tags.outputs.arm64_image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set image tag
|
||||
id: set-tags
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||
ARM64_IMAGE="${{ inputs.image_name }}:$REF_NAME-arm64"
|
||||
echo "arm64_image=$ARM64_IMAGE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push ARM64
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ${{ inputs.dockerfile }}
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.set-tags.outputs.arm64_image }}
|
||||
target: ${{ inputs.target }}
|
||||
|
||||
- name: Set job output
|
||||
run: |
|
||||
echo "image=${{ steps.set-tags.outputs.arm64_image }}" >> $GITHUB_OUTPUT
|
||||
|
||||
name: image-${{ steps.prep.outputs.safe_platform }}
|
||||
path: image.txt
|
||||
retention-days: 1
|
||||
|
||||
create-manifest:
|
||||
name: Create Multi-Arch Manifest
|
||||
name: Create multi-arch manifest
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
needs: build
|
||||
steps:
|
||||
- uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: image-linux-amd64
|
||||
path: /tmp/digests/amd64
|
||||
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: image-linux-arm64
|
||||
path: /tmp/digests/arm64
|
||||
|
||||
- name: Login to Docker
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Create Docker manifest
|
||||
- name: Generate semantic tags
|
||||
id: tags
|
||||
run: |
|
||||
REF_NAME=${GITHUB_REF#refs/tags/}
|
||||
IMAGE="${{ inputs.image_name }}:$REF_NAME"
|
||||
AMD64_IMAGE="${{ needs.build-amd64.outputs.image }}"
|
||||
ARM64_IMAGE="${{ needs.build-arm64.outputs.image }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
|
||||
|
||||
docker manifest create $IMAGE $AMD64_IMAGE $ARM64_IMAGE
|
||||
docker manifest push $IMAGE
|
||||
|
||||
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
||||
docker manifest create ${{ inputs.image_name }}:latest $AMD64_IMAGE $ARM64_IMAGE
|
||||
docker manifest push ${{ inputs.image_name }}:latest
|
||||
echo "VERSION_TAG=$VERSION" >> $GITHUB_OUTPUT
|
||||
if [ -n "$MINOR" ]; then
|
||||
echo "MINOR_TAG=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ -n "$MAJOR" ]; then
|
||||
echo "MAJOR_TAG=$MAJOR" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
if [ "${{ inputs.add_latest }}" = "true" ]; then
|
||||
echo "LATEST_TAG=latest" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ inputs.image_name }}
|
||||
tags: |
|
||||
type=raw,value=${{ steps.tags.outputs.VERSION_TAG }}
|
||||
type=raw,value=${{ steps.tags.outputs.MINOR_TAG }}
|
||||
type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }}
|
||||
type=raw,value=${{ steps.tags.outputs.LATEST_TAG }}
|
||||
|
||||
- name: Create and push manifest list
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
|
||||
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||
echo $TAG_ARGS
|
||||
docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
name: GitHub Release
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
prerelease:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
make_latest:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
discord_title:
|
||||
required: true
|
||||
type: string
|
||||
discord_color:
|
||||
required: true
|
||||
type: number
|
||||
discord_footer:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
DISCORD_WEBHOOK:
|
||||
required: true
|
||||
GH_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Build Changelog
|
||||
id: build_changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v5
|
||||
with:
|
||||
mode: "COMMIT"
|
||||
configurationJson: |
|
||||
{
|
||||
"template": "#{{CHANGELOG}}",
|
||||
"categories": [
|
||||
{
|
||||
"title": "## Feature",
|
||||
"labels": ["feat", "feature"]
|
||||
},
|
||||
{
|
||||
"title": "## Fix",
|
||||
"labels": ["fix", "bug"]
|
||||
},
|
||||
{
|
||||
"title": "## Other",
|
||||
"labels": []
|
||||
}
|
||||
],
|
||||
"label_extractor": [
|
||||
{
|
||||
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)",
|
||||
"on_property": "title",
|
||||
"target": "$1"
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
generate_release_notes: false
|
||||
body: ${{ steps.build_changelog.outputs.changelog }}
|
||||
prerelease: ${{ inputs.prerelease }}
|
||||
make_latest: ${{ inputs.make_latest }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Send Discord Notification
|
||||
env:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
|
||||
|
||||
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
|
||||
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
|
||||
|
||||
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
|
||||
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
|
||||
|
||||
AUTHOR_NAME="Portabase"
|
||||
AUTHOR_ICON="https://github.com/Portabase.png"
|
||||
|
||||
PAYLOAD=$(jq -n \
|
||||
--arg title "$RELEASE_TITLE" \
|
||||
--arg description "$RELEASE_BODY" \
|
||||
--arg url "$RELEASE_URL" \
|
||||
--arg author "$AUTHOR_NAME" \
|
||||
--arg icon "$AUTHOR_ICON" \
|
||||
--arg discord_title "${{ inputs.discord_title }}" \
|
||||
--arg discord_footer "${{ inputs.discord_footer }}" \
|
||||
--argjson discord_color ${{ inputs.discord_color }} \
|
||||
'{
|
||||
content: $discord_title,
|
||||
embeds: [{
|
||||
title: $title,
|
||||
url: $url,
|
||||
description: $description,
|
||||
color: $discord_color,
|
||||
author: {
|
||||
name: $author,
|
||||
icon_url: $icon
|
||||
},
|
||||
footer: {
|
||||
text: $discord_footer
|
||||
}
|
||||
}]
|
||||
}'
|
||||
)
|
||||
|
||||
curl -H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"$DISCORD_WEBHOOK"
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Publish Helm Chart
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
GH_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
publish-helm:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@v4
|
||||
|
||||
- name: Package Helm chart
|
||||
run: |
|
||||
mkdir -p ./helm-packages
|
||||
helm package helm \
|
||||
--version ${{ inputs.version }} \
|
||||
--app-version ${{ inputs.version }} \
|
||||
--destination ./helm-packages
|
||||
|
||||
- name: Authenticate to GitHub Packages
|
||||
run: |
|
||||
echo "${{ secrets.GH_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
|
||||
|
||||
- name: Push Helm chart to GitHub Packages (OCI)
|
||||
run: |
|
||||
helm push ./helm-packages/portabase-agent-${{ inputs.version }}.tgz oci://ghcr.io/portabase/charts
|
||||
+128
-12
@@ -1,32 +1,148 @@
|
||||
name: Publish Docker image for release
|
||||
name: Auto Release & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*.*.*'
|
||||
- '!*-*'
|
||||
pull_request:
|
||||
types: [ closed ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker_publish:
|
||||
|
||||
check-skip:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skip: ${{ steps.set-skip.outputs.skip }}
|
||||
steps:
|
||||
- name: Determine if release should be skipped
|
||||
id: set-skip
|
||||
run: |
|
||||
TITLE="${{ github.event.pull_request.title }}"
|
||||
echo "PR title: $TITLE"
|
||||
|
||||
if [[ "$TITLE" == *"[skip-release]"* ]]; then
|
||||
echo "PR title contains [skip-release], skipping release jobs."
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
create-release:
|
||||
needs: check-skip
|
||||
if: ${{ needs.check-skip.outputs.skip == 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
|
||||
version: ${{ steps.release_step.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/create-github-app-token@v1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ vars.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
ref: main
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install cargo-edit
|
||||
run: cargo install cargo-edit
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
|
||||
- name: Install release-it globally
|
||||
run: |
|
||||
npm install -g release-it
|
||||
npm install -g @release-it/conventional-changelog
|
||||
npm install -g @release-it/bumper
|
||||
|
||||
- run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
|
||||
- name: Run release-it
|
||||
id: release_step
|
||||
run: |
|
||||
git pull origin main
|
||||
|
||||
VERSION=$(release-it --ci --release-version)
|
||||
echo $VERSION
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
OUTPUT=$(release-it --ci)
|
||||
echo "$OUTPUT"
|
||||
|
||||
DRAFT_TAG=$(echo "$OUTPUT" | grep -oE 'untagged-[a-z0-9]+')
|
||||
echo $DRAFT_TAG
|
||||
echo "draft_tag=$DRAFT_TAG" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
publish-docker:
|
||||
needs: create-release
|
||||
if: ${{ needs.create-release.result == 'success' }}
|
||||
uses: ./.github/workflows/docker.yml
|
||||
with:
|
||||
version: ${{ needs.create-release.outputs.version }}
|
||||
ref: ${{ needs.create-release.outputs.version }}
|
||||
add_latest: true
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
github_release:
|
||||
needs: docker_publish
|
||||
uses: ./.github/workflows/github.yml
|
||||
publish-helm:
|
||||
needs: create-release
|
||||
if: ${{ needs.create-release.result == 'success' }}
|
||||
uses: ./.github/workflows/helm.yml
|
||||
with:
|
||||
prerelease: false
|
||||
make_latest: true
|
||||
version: ${{ needs.create-release.outputs.version }}
|
||||
secrets:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
finalize-release:
|
||||
needs:
|
||||
- create-release
|
||||
- publish-docker
|
||||
- publish-helm
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
release_tag: ${{ steps.publish_release_step.outputs.release_tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Publish GitHub Release
|
||||
id: publish_release_step
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
OUTPUT=$(gh release edit ${{ needs.create-release.outputs.draft_tag }} --draft=false)
|
||||
echo "$OUTPUT"
|
||||
RELEASE_TAG=$(echo "$OUTPUT" | sed -E 's|.*/releases/tag/||')
|
||||
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
notify-discord:
|
||||
needs:
|
||||
- publish-docker
|
||||
- publish-helm
|
||||
- create-release
|
||||
- finalize-release
|
||||
uses: ./.github/workflows/discord.yml
|
||||
with:
|
||||
release_tag: ${{ needs.create-release.outputs.version }}
|
||||
discord_title: "||@release-agent|| New release published"
|
||||
discord_color: 5814783
|
||||
discord_color: 3066993
|
||||
discord_footer: "Portabase"
|
||||
secrets:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"github": {
|
||||
"release": true,
|
||||
"draft": true,
|
||||
"tokenRef": "GITHUB_TOKEN"
|
||||
},
|
||||
"git": {
|
||||
"commit": true,
|
||||
"commitMessage": "chore: release ${version}",
|
||||
"requireCleanWorkingDir": true,
|
||||
"tag": true,
|
||||
"tagName": "${version}",
|
||||
"push": true
|
||||
},
|
||||
"hooks": {
|
||||
"before:bump": "cargo set-version ${version}"
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/conventional-changelog": {
|
||||
"preset": {
|
||||
"name": "conventionalcommits",
|
||||
"types": [
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "✨ Features"
|
||||
},
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "🐛 Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "⚡️ Performance Improvements"
|
||||
},
|
||||
{
|
||||
"type": "revert",
|
||||
"section": "⏪️ Reverts"
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"section": "📝 Documentation",
|
||||
"hidden": true
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "🔧 Chores",
|
||||
"hidden": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"@release-it/bumper": {
|
||||
"out": [
|
||||
{
|
||||
"file": "CITATION.cff",
|
||||
"path": "version",
|
||||
"type": "text/yaml"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-4
@@ -1,6 +1,6 @@
|
||||
cff-version: 1.2.0
|
||||
title: Portabase Agent (Rust)
|
||||
message: "If you use this software, please cite it as below."
|
||||
message: If you use this software, please cite it as below.
|
||||
type: software
|
||||
authors:
|
||||
- family-names: Gauthereau
|
||||
@@ -9,7 +9,12 @@ authors:
|
||||
given-names: Killian
|
||||
repository-code: https://github.com/Portabase/agent-rust
|
||||
url: https://portabase.io
|
||||
abstract: "Portabase is a free, open-source, self-hosted solution for database administration, providing backup and restore capabilities, scheduling, retention policies, notifications, and support for multiple storage backends. Its headless agent architecture enables connection to multiple database instances securely and efficiently."
|
||||
abstract: >-
|
||||
Portabase is a free, open-source, self-hosted solution for database
|
||||
administration, providing backup and restore capabilities, scheduling,
|
||||
retention policies, notifications, and support for multiple storage backends.
|
||||
Its headless agent architecture enables connection to multiple database
|
||||
instances securely and efficiently.
|
||||
keywords:
|
||||
- database
|
||||
- administration
|
||||
@@ -22,5 +27,5 @@ keywords:
|
||||
- self-hosted
|
||||
- portabase
|
||||
license: Apache-2.0
|
||||
version: 1.0.4-rc.14
|
||||
date-released: "2026-01-28"
|
||||
version: 1.6.3
|
||||
date-released: '2026-02-24'
|
||||
|
||||
Generated
+2505
-281
File diff suppressed because it is too large
Load Diff
+37
-5
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "portabase-agent"
|
||||
version = "1.0.4-rc.14"
|
||||
version = "1.6.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -17,7 +17,7 @@ base64 = "0.22.1"
|
||||
thiserror = "2.0.17"
|
||||
log = "0.4.29"
|
||||
toml = "0.9.10"
|
||||
reqwest = { version = "0.13.1", features = ["json", "blocking", "multipart"] }
|
||||
reqwest = { version = "0.13.1", features = ["json", "blocking", "multipart", "stream", "query"] }
|
||||
anyhow = "1.0.100"
|
||||
tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros", "fs"] }
|
||||
async-trait = "0.1.89"
|
||||
@@ -28,11 +28,43 @@ flate2 = "1.1.5"
|
||||
tar = "0.4.44"
|
||||
tokio-postgres = "0.7.15"
|
||||
futures = "0.3.31"
|
||||
tracing-log = "0.2.0"
|
||||
tracing-appender = "0.2.4"
|
||||
time = { version = "0.3.44", features = ["macros"] }
|
||||
mongodb = "3.5.0"
|
||||
mongodb = "3.5.1"
|
||||
rand = "0.9.2"
|
||||
bytes = "1.11.0"
|
||||
async-stream = "0.3.6"
|
||||
uuid = { version = "1.20.0", features = ["v4"] }
|
||||
tokio-util = "0.7.18"
|
||||
aws-config = "1.8.13"
|
||||
aws-sdk-s3 = { version = "1.122.0", features = ["behavior-version-latest"] }
|
||||
async-compression = { version = "0.4.37", features = ["tokio", "gzip"] }
|
||||
tokio-tar = "0.3.1"
|
||||
oauth2 = "5.0.0"
|
||||
hyper = "1.8.1"
|
||||
aes-gcm = "0.11.0-rc.3"
|
||||
generic-array = "0.14.7"
|
||||
futures-util = "0.3.31"
|
||||
tokio-stream = "0.1.18"
|
||||
aes = "0.9.0-rc.4"
|
||||
typenum = "1.19.0"
|
||||
testcontainers = "0.27.1"
|
||||
testcontainers-modules = { version = "0.15.0", features = ["postgres", "redis", "valkey", "mysql", "mariadb", "mongo"] }
|
||||
postgres = "0.19.12"
|
||||
url = "2.5.8"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
mockall = "0.13"
|
||||
testcontainers = "0.27.1"
|
||||
testcontainers-modules = { version = "0.15.0", features = ["postgres", "redis", "mysql", "mariadb", "mongo"] }
|
||||
wiremock = "0.6"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = "debuginfo"
|
||||
|
||||
[[bin]]
|
||||
name = "app"
|
||||
path = "src/main.rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
include .env
|
||||
export $(shell sed 's/=.*//' .env)
|
||||
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
|
||||
|
||||
.PHONY: seed-mongo seed-mysql seed-postgres
|
||||
|
||||
up:
|
||||
@bash $(CLUSTER_SCRIPT)
|
||||
|
||||
seed-mongo:
|
||||
@echo "Seeding MongoDB..."
|
||||
bash ./scripts/mongo/seed-mongo.sh
|
||||
@@ -15,6 +19,10 @@ seed-mysql:
|
||||
@echo "Seeding MySQL..."
|
||||
mysql -h 127.0.0.1 -P "$$MYSQL_PORT" -u "$$MYSQL_USER" -p"$$MYSQL_PASSWORD" "$$MYSQL_DB" < ./scripts/mysql/seed-mysql.sql
|
||||
|
||||
seed-mysql-1gb:
|
||||
@echo "Seeding MySQL..."
|
||||
mysql -h 127.0.0.1 -P "$$MYSQL_PORT" -u "$$MYSQL_USER" -p"$$MYSQL_PASSWORD" "$$MYSQL_DB" < ./scripts/mysql/seed-1gb.sql
|
||||
|
||||
|
||||
seed-postgres:
|
||||
@echo "Seeding Postgres..."
|
||||
@@ -26,4 +34,21 @@ seed-postgres-1gb:
|
||||
docker exec -i -e PGPASSWORD=$$PG_PASSWORD $$PG_CONTAINER \
|
||||
psql -U $$PG_USER -d $$PG_DB < ./scripts/postgres/seed-1gb.sql
|
||||
|
||||
|
||||
SQLITE_SEED_FILE := $(if $(filter big,$(SEED)),./scripts/sqlite/seed-big.sql,./scripts/sqlite/seed.sql)
|
||||
|
||||
seed-sqlite:
|
||||
@echo "Seeding Sqlite..."
|
||||
@echo "Run as root to fix permissions inside the volume"
|
||||
docker exec -u 0 -it db-sqlite sh -c "chmod -R 777 /workspace/data"
|
||||
@echo "Create the database file (if it doesn’t exist)"
|
||||
docker exec -u 0 -it db-sqlite sh -c "touch /workspace/data/app.db"
|
||||
@echo "Seed the database"
|
||||
docker exec -i db-sqlite sh -c "sqlite3 /workspace/data/app.db" < $(SQLITE_SEED_FILE)
|
||||
@echo "Verify"
|
||||
docker exec -it db-sqlite sqlite3 /workspace/data/app.db "SELECT name FROM sqlite_master WHERE type='table';"
|
||||
@echo "Done"
|
||||
|
||||
|
||||
|
||||
seed-all: seed-mongo seed-mysql seed-postgres seed-postgres-1gb
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
[](https://github.com/Portabase/portabase)
|
||||
[](https://www.buymeacoffee.com/portabase)
|
||||
|
||||
[](https://www.python.org/downloads/release/python-3120/)
|
||||
[](https://www.postgresql.org/)
|
||||
[](https://www.mysql.com/)
|
||||
[](https://sqlite.org/)
|
||||
[](https://redis.io/)
|
||||
[](https://valkey.io/)
|
||||
[](https://mariadb.org/)
|
||||
[](https://www.mongodb.com/)
|
||||
[](https://github.com/Portabase/portabase)
|
||||
@@ -79,3 +81,5 @@ Distributed under the Apache License. See `LICENSE.txt` for more information.
|
||||
|
||||
[Docker-url]: https://www.docker.com/
|
||||
|
||||
|
||||
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+44
-2
@@ -34,11 +34,53 @@
|
||||
"name": "Test database 5 - MongoDB",
|
||||
"database": "testdb",
|
||||
"type": "mongodb",
|
||||
"username": "",
|
||||
"password": "",
|
||||
"port": 27017,
|
||||
"host": "db-mongodb",
|
||||
"generated_id": "16678147-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 6 - SQLite DB",
|
||||
"type": "sqlite",
|
||||
"path": "/sqlite-data/workspace/data/app.db",
|
||||
"generated_id": "16678178-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 7 - SQLite DB",
|
||||
"type": "sqlite",
|
||||
"path": "/sqlite-data-2/workspace/data/app.db",
|
||||
"generated_id": "16678179-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 8 - Redis",
|
||||
"type": "redis",
|
||||
"port": 6379,
|
||||
"host": "db-redis",
|
||||
"generated_id": "16678166-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 9 - Redis Auth",
|
||||
"type": "redis",
|
||||
"password": "supersecurepassword",
|
||||
"port": 6379,
|
||||
"username": "default",
|
||||
"host": "db-redis-auth",
|
||||
"generated_id": "16678160-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 10 - Valkey",
|
||||
"type": "valkey",
|
||||
"port": 6379,
|
||||
"host": "db-valkey",
|
||||
"generated_id": "16678560-ff7e-4c97-8c83-0adeff214681"
|
||||
},
|
||||
{
|
||||
"name": "Test database 11 - Valkey Auth",
|
||||
"type": "valkey",
|
||||
"password": "supersecurepassword",
|
||||
"port": 6379,
|
||||
"username": "default",
|
||||
"host": "db-valkey-auth",
|
||||
"generated_id": "16678561-ff7e-4c97-8c83-0adeff214681"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+48
-7
@@ -1,11 +1,11 @@
|
||||
services:
|
||||
rust-app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: prod
|
||||
# image: portabase/agent:latest
|
||||
platform: linux/arm64
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: docker/Dockerfile
|
||||
# target: prod
|
||||
image: portabase/agent:latest
|
||||
# platform: linux/arm64
|
||||
container_name: rust-prod
|
||||
volumes:
|
||||
- ./databases.json:/config/config.json
|
||||
@@ -14,7 +14,7 @@ services:
|
||||
LOG: info
|
||||
TZ: "Europe/Paris"
|
||||
# DATABASES_CONFIG_FILE: "config.toml"
|
||||
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiY2VlZmNmNDQtOGE0YS00NjZlLTkwNDEtN2QzNDMzZjRjOTJkIiwicHVibGljS2V5IjoiLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tXG5NSUlCQ2dLQ0FRRUFuYUVKdnVBSExlNGF5d0RmNGplelVobm45VXRkdksyZ3pEMEg2cERJYXczYkJKRkpwVnVDXG5uVFV3MXA3Q2RnOXBzdjZhRnpyOXZPd0J2MjMzckxpdVpCT2lCb2p2Q0QrSlZid3hyTzBRRW5hN2dmaHV1ZGYwXG5VVlJOMkxmK1g1aTkvZzJTNm5xcExoTm1DaGFJNk8ybktYZUNlRmtubEErRUJrNnFoV1FCVGozb05TYTFTOFY1XG40UFRTT2I4NUo3a2k5YllEbXRiNWxrU3dCNXdXOTdtQjg0ZzI2WHAvU3FFcmhKc0NGK3YrN09vTWYzTzJqTTNoXG5XMUQ0MzBPRitWaklwUGdoV09rZy96NXZQUWFHRzhqQ0h4VDlJR0Q0bjhyS05LQ3FTOGNyN2diTGU0cWpNdmhvXG5BQVVvaHpHR2FRNkhlWlJ4S0UvM3J1a2JldnY5dnJ2TTNRSURBUUFCXG4tLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tXG4ifQ"
|
||||
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNWU1OGU2MGEtODhiMy00YTBjLWI0NDktNTQ3OWZhOTQzZDBkIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
|
||||
extra_hosts:
|
||||
- "localhost:host-gateway"
|
||||
networks:
|
||||
@@ -22,6 +22,47 @@ services:
|
||||
|
||||
|
||||
|
||||
db-mongodb-auth:
|
||||
container_name: db-mongodb-auth
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "27082:27017"
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
MONGO_INITDB_ROOT_PASSWORD: rootpassword
|
||||
MONGO_INITDB_DATABASE: testdbauth
|
||||
command: mongod --auth
|
||||
networks:
|
||||
- portabase
|
||||
volumes:
|
||||
- mongodb-data-auth:/data/db
|
||||
healthcheck:
|
||||
test: [ "CMD", "mongo", "--eval", "db.adminCommand('ping')" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
db-mongodb:
|
||||
container_name: db-mongodb
|
||||
image: mongo:latest
|
||||
ports:
|
||||
- "27083:27017"
|
||||
volumes:
|
||||
- mongodb-data:/data/db
|
||||
healthcheck:
|
||||
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
environment:
|
||||
MONGO_INITDB_DATABASE: testdb
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
volumes:
|
||||
mongodb-data:
|
||||
mongodb-data-auth:
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
name: portabase_network
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
agent-test:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
target: dev
|
||||
working_dir: /app
|
||||
container_name: agent-test
|
||||
volumes:
|
||||
- .:/app
|
||||
- cargo-registry:/usr/local/cargo/registry
|
||||
- cargo-git:/usr/local/cargo/git
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
APP_ENV: test
|
||||
LOG: debug
|
||||
TZ: Europe/Paris
|
||||
EDGE_KEY: ""
|
||||
|
||||
volumes:
|
||||
cargo-registry:
|
||||
cargo-git:
|
||||
+130
-66
@@ -11,12 +11,16 @@ services:
|
||||
# - ./databases.toml:/config/config.toml
|
||||
- cargo-registry:/usr/local/cargo/registry
|
||||
- cargo-git:/usr/local/cargo/git
|
||||
- cargo-target:/app/target
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
# - cargo-target:/app/target
|
||||
# - sqlite-data:/sqlite-data/workspace/data
|
||||
# - ./scripts/sqlite/test-db:/sqlite-data-2/workspace/data
|
||||
environment:
|
||||
APP_ENV: development
|
||||
LOG: debug
|
||||
TZ: "Europe/Paris"
|
||||
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiOGZmMDE4NTQtYjJhMS00ZTE0LTkwMjctZTJiOWIxZjQ1YzdlIiwicHVibGljS2V5IjoiLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tXG5NSUlCQ2dLQ0FRRUE5TWV4M2pmdnVLdFB5YU1ERnh2Ulp2dmd3YkRJQ2JzQi81Wll5NDNSVVRBaXZRYjJiSDdYXG5qRHBQd1lJeCs4UFBrbHlRbDVMQzV1UWZEaCs4SVd4OG1LZ3FvMXpWMkdiZXdGbEdEWFYxVEdyU1ZEU25aSWR4XG52bWdYc29EeXhVMlJvWUFUMS9YMWxuc2YxenZKdkFMTkhXdEhRdk42SjVDZTFSMmFsendVRGFEVXlJNzRmSldQXG5tNTh0SDMrYklXL0VVTXdjaWNxM0oySWw3Vm9KNkZNUHJQL1ZSOWEvdFF1SU1qa200MXpFY2NscExPa2luRkxuXG54NmVUWkFSZUpya2UrbnRvZ2t4TGEyRWV5a1lUNzB4V3hKNWp5ZExBVnRvNkkyQlVLVVJoTkowTUFaU29NYUtvXG5iMGJRcnY1UzExZWllMnMrT2I3aTYzSFpkVUx0UmV1MVJ3SURBUUFCXG4tLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tXG4ifQ"
|
||||
EDGE_KEY: "eyJzZXJ2ZXJVcmwiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODciLCJhZ2VudElkIjoiNGI1OTM2MGItNTNkMi00ZTZmLWE1ODctODcyMmQ1NDc1MTNmIiwibWFzdGVyS2V5QjY0IjoiQlhWM1hvbEM2NTZTVjdkTmdjV1BHUWxrKytycExJNmxHRGk3Q1BCNWllbz0ifQ=="
|
||||
#POOLING: 1
|
||||
#DATABASES_CONFIG_FILE: "config.toml"
|
||||
extra_hosts:
|
||||
@@ -24,87 +28,147 @@ services:
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
# db-postgres:
|
||||
# container_name: db-postgres
|
||||
# image: postgres:17-alpine
|
||||
# ports:
|
||||
# - "5436:5432"
|
||||
# volumes:
|
||||
# - postgres-data:/var/lib/postgresql/data
|
||||
# environment:
|
||||
# - POSTGRES_DB=devdb
|
||||
# - POSTGRES_USER=devuser
|
||||
# - POSTGRES_PASSWORD=changeme
|
||||
# networks:
|
||||
# - portabase
|
||||
#
|
||||
# db-mariadb:
|
||||
# container_name: db-mariadb
|
||||
# image: mariadb:latest
|
||||
# ports:
|
||||
# - "3311:3306"
|
||||
# environment:
|
||||
# - MYSQL_DATABASE=mariadb
|
||||
# - MYSQL_USER=mariadb
|
||||
# - MYSQL_PASSWORD=changeme
|
||||
# - MYSQL_RANDOM_ROOT_PASSWORD=yes
|
||||
# volumes:
|
||||
# - mariadb-data:/var/lib/mysql
|
||||
# networks:
|
||||
# - portabase
|
||||
|
||||
|
||||
db-mongodb-auth:
|
||||
container_name: db-mongodb-auth
|
||||
image: mongo:latest
|
||||
db-postgres:
|
||||
container_name: db-postgres
|
||||
image: postgres:17-alpine
|
||||
ports:
|
||||
- "27082:27017"
|
||||
- "5436:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: root
|
||||
MONGO_INITDB_ROOT_PASSWORD: rootpassword
|
||||
MONGO_INITDB_DATABASE: testdbauth
|
||||
command: mongod --auth
|
||||
- POSTGRES_DB=devdb
|
||||
- POSTGRES_USER=devuser
|
||||
- POSTGRES_PASSWORD=changeme
|
||||
networks:
|
||||
- portabase
|
||||
volumes:
|
||||
- mongodb-data-auth:/data/db
|
||||
healthcheck:
|
||||
test: [ "CMD", "mongo", "--eval", "db.adminCommand('ping')" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
#
|
||||
# db-mariadb:
|
||||
# container_name: db-mariadb
|
||||
# image: mariadb:latest
|
||||
# ports:
|
||||
# - "3311:3306"
|
||||
# environment:
|
||||
# - MYSQL_DATABASE=mariadb
|
||||
# - MYSQL_USER=mariadb
|
||||
# - MYSQL_PASSWORD=changeme
|
||||
# - MYSQL_RANDOM_ROOT_PASSWORD=yes
|
||||
# volumes:
|
||||
# - mariadb-data:/var/lib/mysql
|
||||
# networks:
|
||||
# - portabase
|
||||
#
|
||||
#
|
||||
# db-mongodb-auth:
|
||||
# container_name: db-mongodb-auth
|
||||
# image: mongo:latest
|
||||
# ports:
|
||||
# - "27082:27017"
|
||||
# environment:
|
||||
# MONGO_INITDB_ROOT_USERNAME: root
|
||||
# MONGO_INITDB_ROOT_PASSWORD: rootpassword
|
||||
# MONGO_INITDB_DATABASE: testdbauth
|
||||
# command: mongod --auth
|
||||
# networks:
|
||||
# - portabase
|
||||
# volumes:
|
||||
# - mongodb-data-auth:/data/db
|
||||
# healthcheck:
|
||||
# test: [ "CMD", "mongo", "--eval", "db.adminCommand('ping')" ]
|
||||
# interval: 5s
|
||||
# timeout: 5s
|
||||
# retries: 10
|
||||
#
|
||||
# db-mongodb:
|
||||
# container_name: db-mongodb
|
||||
# image: mongo:latest
|
||||
# ports:
|
||||
# - "27083:27017"
|
||||
# volumes:
|
||||
# - mongodb-data:/data/db
|
||||
# healthcheck:
|
||||
# test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
|
||||
# interval: 5s
|
||||
# timeout: 5s
|
||||
# retries: 10
|
||||
# environment:
|
||||
# MONGO_INITDB_DATABASE: testdb
|
||||
# networks:
|
||||
# - portabase
|
||||
|
||||
db-mongodb:
|
||||
container_name: db-mongodb
|
||||
image: mongo:latest
|
||||
# sqlite:
|
||||
# container_name: db-sqlite
|
||||
# image: keinos/sqlite3
|
||||
# volumes:
|
||||
# - sqlite-data:/workspace/data
|
||||
# working_dir: /workspace
|
||||
# command: tail -f /dev/null
|
||||
# stdin_open: true
|
||||
# tty: true
|
||||
|
||||
db-redis:
|
||||
image: redis:latest
|
||||
container_name: db-redis
|
||||
ports:
|
||||
- "27083:27017"
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- mongodb-data:/data/db
|
||||
healthcheck:
|
||||
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')" ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
- redis-data:/data
|
||||
command: [ "redis-server", "--appendonly", "yes" ]
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
db-redis-auth:
|
||||
image: redis:latest
|
||||
container_name: db-redis-auth
|
||||
ports:
|
||||
- "6380:6379"
|
||||
volumes:
|
||||
- redis-data-auth:/data
|
||||
environment:
|
||||
MONGO_INITDB_DATABASE: testdb
|
||||
- REDIS_PASSWORD=supersecurepassword
|
||||
command: [ "redis-server", "--requirepass", "supersecurepassword", "--appendonly", "yes" ]
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
db-valkey:
|
||||
image: valkey/valkey
|
||||
container_name: db-valkey
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
ports:
|
||||
- '6381:6379'
|
||||
volumes:
|
||||
- valkey-data:/data
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
db-valkey-auth:
|
||||
image: valkey/valkey
|
||||
container_name: db-valkey-auth
|
||||
command: >
|
||||
--requirepass "supersecurepassword"
|
||||
ports:
|
||||
- '6382:6379'
|
||||
volumes:
|
||||
- valkey-data-auth:/data
|
||||
networks:
|
||||
- portabase
|
||||
|
||||
volumes:
|
||||
cargo-registry:
|
||||
cargo-git:
|
||||
cargo-target:
|
||||
|
||||
|
||||
# postgres-data:
|
||||
# mariadb-data:
|
||||
mongodb-data:
|
||||
mongodb-data-auth:
|
||||
# cargo-target:
|
||||
postgres-data:
|
||||
# mariadb-data:
|
||||
# mongodb-data:
|
||||
# mongodb-data-auth:
|
||||
# sqlite-data:
|
||||
redis-data:
|
||||
redis-data-auth:
|
||||
valkey-data:
|
||||
valkey-data-auth:
|
||||
|
||||
networks:
|
||||
portabase:
|
||||
name: portabase_network
|
||||
external: true
|
||||
|
||||
# docker network create portabase_network
|
||||
+8
-125
@@ -1,7 +1,7 @@
|
||||
# =========================
|
||||
# Base image (shared)
|
||||
# =========================
|
||||
FROM rust:1.92.0 AS base
|
||||
FROM rust:1.94.0 AS base
|
||||
|
||||
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
pkg-config \
|
||||
@@ -15,6 +15,9 @@ RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
zlib1g \
|
||||
curl \
|
||||
mariadb-client \
|
||||
sqlite3 \
|
||||
redis-tools \
|
||||
valkey \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -57,8 +60,8 @@ WORKDIR /app
|
||||
FROM base AS dev
|
||||
|
||||
RUN cargo install cargo-watch
|
||||
RUN cargo install grcov --locked
|
||||
|
||||
# Pre-cache dependencies
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||
RUN cargo build
|
||||
@@ -93,6 +96,9 @@ RUN apt-get update && apt-get install -y \
|
||||
libncurses6 \
|
||||
zlib1g \
|
||||
mariadb-client \
|
||||
sqlite3 \
|
||||
redis-tools \
|
||||
valkey \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -110,126 +116,3 @@ COPY --from=base /usr/local/mongodb/bin/ /usr/local/mongodb/bin/
|
||||
ENV APP_ENV=production
|
||||
|
||||
CMD ["/entrypoint.sh"]
|
||||
|
||||
# =========================
|
||||
# Base image (shared)
|
||||
# =========================
|
||||
#FROM rust:1.92.0 AS base
|
||||
#
|
||||
#RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \
|
||||
# pkg-config \
|
||||
# libssl-dev \
|
||||
# tzdata \
|
||||
# redis-server \
|
||||
# ca-certificates \
|
||||
# libpq5 \
|
||||
# libreadline8 \
|
||||
# libncurses6 \
|
||||
# zlib1g \
|
||||
# curl \
|
||||
# mariadb-client \
|
||||
# && apt-get clean \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
#
|
||||
#ARG TARGETARCH
|
||||
#
|
||||
## =========================
|
||||
## PostgreSQL client binaries (versions 12-18)
|
||||
## =========================
|
||||
#RUN for v in 12 13 14 15 16 17 18; do mkdir -p /usr/lib/postgresql/$v/bin; done
|
||||
#
|
||||
#COPY assets/tools/amd64/postgresql/ /tmp/pg-x64/
|
||||
#COPY assets/tools/arm64/postgresql/ /tmp/pg-arm/
|
||||
#
|
||||
#RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
# for v in 12 13 14 15 16 17 18; do \
|
||||
# cp -r /tmp/pg-x64/postgresql-$v/bin/* /usr/lib/postgresql/$v/bin/; \
|
||||
# done; \
|
||||
# else \
|
||||
# for v in 12 13 14 15 16 17 18; do \
|
||||
# cp -r /tmp/pg-arm/postgresql-$v/bin/* /usr/lib/postgresql/$v/bin/; \
|
||||
# done; \
|
||||
# fi && \
|
||||
# rm -rf /tmp/pg-x64 /tmp/pg-arm && \
|
||||
# chmod +x /usr/lib/postgresql/*/bin/*
|
||||
#
|
||||
## =========================
|
||||
## MongoDB client binaries
|
||||
## =========================
|
||||
#COPY assets/tools/${TARGETARCH}/mongodb/ /usr/local/mongodb/
|
||||
#RUN chmod +x /usr/local/mongodb/bin/*
|
||||
#
|
||||
#WORKDIR /app
|
||||
#
|
||||
## =========================
|
||||
## Development image
|
||||
## =========================
|
||||
#FROM base AS dev
|
||||
#
|
||||
#RUN cargo install cargo-watch
|
||||
#
|
||||
#COPY Cargo.toml Cargo.lock ./
|
||||
#RUN mkdir src && echo "fn main() {}" > src/main.rs
|
||||
#RUN cargo build
|
||||
#RUN rm -rf src
|
||||
#
|
||||
#COPY entrypoint.sh /entrypoint.sh
|
||||
#RUN chmod +x /entrypoint.sh
|
||||
#
|
||||
#CMD ["/entrypoint.sh"]
|
||||
#
|
||||
## =========================
|
||||
## Builder (production)
|
||||
## =========================
|
||||
#FROM base AS builder
|
||||
#
|
||||
#ARG TARGETARCH
|
||||
#
|
||||
#RUN rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
|
||||
#
|
||||
#COPY . .
|
||||
#
|
||||
#RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
# cargo build --release --target x86_64-unknown-linux-gnu && \
|
||||
# mv /app/target/x86_64-unknown-linux-gnu/release /app/target/release-app; \
|
||||
# else \
|
||||
# cargo build --release --target aarch64-unknown-linux-gnu && \
|
||||
# mv /app/target/aarch64-unknown-linux-gnu/release /app/target/release-app; \
|
||||
# fi
|
||||
#
|
||||
#
|
||||
#RUN echo "APP_VERSION=$(cargo pkgid | awk -F# '{print $2}')" > /app/version.env
|
||||
#
|
||||
## =========================
|
||||
## Runtime (production)
|
||||
## =========================
|
||||
#FROM ubuntu:24.04 AS prod
|
||||
#
|
||||
#RUN apt-get update && apt-get install -y \
|
||||
# redis-server \
|
||||
# ca-certificates \
|
||||
# tzdata \
|
||||
# libpq5 \
|
||||
# libreadline8 \
|
||||
# libncurses6 \
|
||||
# zlib1g \
|
||||
# mariadb-client \
|
||||
# && rm -rf /var/lib/apt/lists/*
|
||||
#
|
||||
#WORKDIR /app
|
||||
#
|
||||
#ARG TARGETARCH
|
||||
#
|
||||
#COPY --from=builder /app/target/release-app/app /usr/local/bin/app
|
||||
#
|
||||
#COPY --from=builder /app/version.env /app/version.env
|
||||
#COPY entrypoint.sh /entrypoint.sh
|
||||
#RUN chmod +x /entrypoint.sh
|
||||
#
|
||||
#COPY --from=base /usr/lib/postgresql/ /usr/lib/postgresql/
|
||||
#COPY --from=base /usr/local/mongodb/bin/ /usr/local/mongodb/bin/
|
||||
#
|
||||
#ENV APP_ENV=production
|
||||
#
|
||||
#CMD ["/entrypoint.sh"]
|
||||
|
||||
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
check_docker() {
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo "Docker is not running. Attempting to start Docker..."
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
open -a Docker
|
||||
echo "Waiting for Docker to start..."
|
||||
until docker info > /dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
elif command -v systemctl >/dev/null 2>&1; then
|
||||
sudo systemctl start docker
|
||||
else
|
||||
echo "Cannot start Docker automatically. Please start Docker manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Docker is running."
|
||||
fi
|
||||
}
|
||||
|
||||
check_network() {
|
||||
local network_name="portabase_network"
|
||||
if ! docker network ls --format '{{.Name}}' | grep -q "^${network_name}$"; then
|
||||
echo "Docker network '${network_name}' not found. Creating..."
|
||||
docker network create "${network_name}"
|
||||
else
|
||||
echo "Docker network '${network_name}' already exists."
|
||||
fi
|
||||
}
|
||||
|
||||
check_docker
|
||||
check_network
|
||||
|
||||
echo "Starting docker-compose..."
|
||||
docker compose -f ./docker-compose.yml up
|
||||
echo "Docker-compose started successfully."
|
||||
+10
-3
@@ -34,16 +34,23 @@ if [ -n "$TZ" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
REDIS_PORT=65515
|
||||
echo "[entrypoint] APP_ENV=$APP_ENV"
|
||||
echo "[entrypoint] Starting Redis..."
|
||||
redis-server --daemonize yes
|
||||
redis-server --port $REDIS_PORT --daemonize yes
|
||||
|
||||
echo "[entrypoint] Waiting for Redis to be ready..."
|
||||
until redis-cli ping >/dev/null 2>&1; do
|
||||
MAX_RETRIES=20
|
||||
COUNT=0
|
||||
until redis-cli -h localhost -p "$REDIS_PORT" ping >/dev/null 2>&1 ; do
|
||||
COUNT=$((COUNT+1))
|
||||
if [ $COUNT -ge $MAX_RETRIES ]; then
|
||||
echo "[ERROR] Redis did not start after $MAX_RETRIES attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "[entrypoint] Redis not ready, sleeping 1s..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "[entrypoint] Redis is ready"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,29 @@
|
||||
apiVersion: v2
|
||||
name: portabase-agent
|
||||
description: Helm chart for Portabase Agent
|
||||
type: application
|
||||
version: 0.0.0
|
||||
appVersion: "latest"
|
||||
keywords:
|
||||
- postgresql
|
||||
- mariadb
|
||||
- mongodb
|
||||
- mysql
|
||||
- sqlite
|
||||
- backup
|
||||
- database
|
||||
- restore
|
||||
- agent
|
||||
home: https://github.com/Portabase/agent
|
||||
|
||||
sources:
|
||||
- https://github.com/Portabase/agent
|
||||
- https://github.com/Portabase/agent/tree/main/helm
|
||||
|
||||
maintainers:
|
||||
- name: Charles Gauthereau
|
||||
url: https://github.com/RambokDev
|
||||
- name: Killian Larcher
|
||||
url: https://github.com/KillianLarcher
|
||||
|
||||
icon: https://raw.githubusercontent.com/Portabase/agent/main/.github/assets/logo.png
|
||||
@@ -0,0 +1,59 @@
|
||||
# Development Notes
|
||||
|
||||
## Check that Kubernetes is reachable locally
|
||||
|
||||
```bash
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
## Install the local Portabase Agent Helm chart
|
||||
|
||||
```bash
|
||||
helm install portabase-agent . \
|
||||
--set env.EDGE_KEY=<your-edge-key>
|
||||
```
|
||||
|
||||
## Check the pods
|
||||
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
|
||||
## Check the services
|
||||
|
||||
```bash
|
||||
kubectl get svc
|
||||
```
|
||||
|
||||
## To update .env variables or JSON config:
|
||||
```bash
|
||||
kubectl rollout restart deployment portabase-agent
|
||||
```
|
||||
|
||||
## Install or upgrade the Helm chart
|
||||
```bash
|
||||
helm upgrade portabase-agent . \
|
||||
--reuse-values \
|
||||
--set env.EDGE_KEY="NEW_EDGE_KEY"
|
||||
```
|
||||
|
||||
## Rollout to restart
|
||||
```bash
|
||||
kubectl rollout restart deployment portabase-agent
|
||||
```
|
||||
|
||||
## List pods to get the pod name
|
||||
|
||||
```bash
|
||||
kubectl get pods -l app=portabase-agent
|
||||
```
|
||||
|
||||
## Get logs for the pod
|
||||
```bash
|
||||
kubectl logs portabase-agent-6f7d4f5c6b-abc12
|
||||
```
|
||||
|
||||
## Uninstall Agent
|
||||
``` bash
|
||||
helm uninstall portabase-agent
|
||||
```
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: portabase-agent-config
|
||||
data:
|
||||
config.json: |
|
||||
{{ .Values.volume.configFile.content | nindent 4 }}
|
||||
@@ -0,0 +1,62 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: portabase-agent
|
||||
labels:
|
||||
app: portabase-agent
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: portabase-agent
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: portabase-agent
|
||||
spec:
|
||||
hostAliases:
|
||||
{{- range .Values.network.hostAliases }}
|
||||
- ip: {{ .ip }}
|
||||
hostnames:
|
||||
{{- range .hostnames }}
|
||||
- {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: portabase-agent
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: portabase-agent-env
|
||||
volumeMounts:
|
||||
{{- if .Values.volume.configFile.enabled }}
|
||||
{{- if .Values.volume.configFile.hostPath }}
|
||||
- name: config
|
||||
mountPath: /config/config.json
|
||||
subPath: config.json
|
||||
readOnly: true
|
||||
# uses hostPath
|
||||
{{- else }}
|
||||
- name: config
|
||||
mountPath: /config/config.json
|
||||
subPath: config.json
|
||||
# uses ConfigMap content
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
{{- if .Values.volume.configFile.enabled }}
|
||||
{{- if .Values.volume.configFile.hostPath }}
|
||||
- name: config
|
||||
hostPath:
|
||||
path: {{ .Values.volume.configFile.hostPath }}
|
||||
type: File
|
||||
{{- else }}
|
||||
- name: config
|
||||
configMap:
|
||||
name: portabase-agent-config
|
||||
items:
|
||||
- key: config.json
|
||||
path: config.json
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: portabase-agent-env
|
||||
data:
|
||||
EDGE_KEY: {{ .Values.env.EDGE_KEY | quote }}
|
||||
TZ: {{ .Values.env.TZ | quote }}
|
||||
POLLING: {{ .Values.env.POLLING | quote }}
|
||||
APP_ENV: {{ .Values.env.APP_ENV | quote }}
|
||||
LOG: {{ .Values.env.LOG | quote }}
|
||||
@@ -0,0 +1,48 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: portabase/agent
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
env:
|
||||
EDGE_KEY: "your_edge_key_here"
|
||||
TZ: "UTC"
|
||||
POLLING: "5"
|
||||
APP_ENV: "production"
|
||||
LOG: "info"
|
||||
|
||||
resources:
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
|
||||
volume:
|
||||
configFile:
|
||||
enabled: true
|
||||
hostPath: "" # Use host file if set, otherwise use `content`
|
||||
content: | # JSON content for config.json if no hostPath
|
||||
{
|
||||
"databases": [
|
||||
{
|
||||
"name": "my-site-prod (readable name)",
|
||||
"database": "devdb",
|
||||
"type": "postgresql",
|
||||
"host": "localhost",
|
||||
"port": 5432,
|
||||
"username": "admin_prod",
|
||||
"password": "super_secure_password",
|
||||
"generated_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
network:
|
||||
hostAliases:
|
||||
- ip: "127.0.0.1"
|
||||
hostnames:
|
||||
- "localhost"
|
||||
@@ -1,99 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: ./release <version>"
|
||||
echo "Example: ./release v1.0.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
if [ "$CURRENT_BRANCH" = "main" ]; then
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
|
||||
echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
CLEAN_VERSION=${VERSION#v}
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
|
||||
echo "Preparing release $VERSION..."
|
||||
|
||||
|
||||
# package.json
|
||||
if [ -f package.json ]; then
|
||||
echo "Updating package.json..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
||||
else
|
||||
sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
|
||||
fi
|
||||
fi
|
||||
|
||||
# pyproject.toml
|
||||
if [ -f pyproject.toml ]; then
|
||||
echo "Updating pyproject.toml..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
||||
else
|
||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cargo.toml
|
||||
if [ -f Cargo.toml ]; then
|
||||
echo "Updating Cargo.toml..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
||||
else
|
||||
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
|
||||
fi
|
||||
fi
|
||||
|
||||
# CITATION.cff
|
||||
if [ -f CITATION.cff ]; then
|
||||
echo "Updating CITATION.cff..."
|
||||
if sed --version >/dev/null 2>&1; then
|
||||
sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
||||
sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
||||
else
|
||||
sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
|
||||
sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
git add .
|
||||
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo "Committing changes..."
|
||||
git commit -m "chore(release): $VERSION"
|
||||
else
|
||||
echo "No changes to commit. Proceeding to tag..."
|
||||
fi
|
||||
|
||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag $VERSION already exists. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating tag $VERSION..."
|
||||
git tag -a "$VERSION" -m "Release $VERSION"
|
||||
|
||||
echo "Pushing changes and tags to remote..."
|
||||
git push
|
||||
git push origin "$VERSION"
|
||||
|
||||
echo "Successfully released $VERSION!"
|
||||
+20
-2
@@ -1,12 +1,30 @@
|
||||
# Seed instructions
|
||||
|
||||
## MongoDB
|
||||
|
||||
```bash
|
||||
make seed-mongo
|
||||
make seed-mongo-auth
|
||||
make seed-mysql
|
||||
make seed-mysql-1gb
|
||||
make seed-postgres
|
||||
make seed-postgres-1gb
|
||||
make seed-all
|
||||
make seed-all
|
||||
make seed-sqlite
|
||||
make seed-sqlite SEED=big
|
||||
```
|
||||
|
||||
## Verify commands
|
||||
|
||||
### Sqlite
|
||||
|
||||
```bash
|
||||
docker exec -it db-sqlite sqlite3 /workspace/data/app.db "SELECT * FROM users LIMIT 10;"
|
||||
```
|
||||
|
||||
```bash
|
||||
docker exec -it db-sqlite sqlite3 /workspace/data/app.db "SELECT name FROM sqlite_master WHERE type='table';"
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yml run agent-test bash -c "cargo clean && cargo test"
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
CREATE DATABASE IF NOT EXISTS mariadb;
|
||||
USE mariadb;
|
||||
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS products;
|
||||
|
||||
CREATE TABLE users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE products (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description MEDIUMTEXT,
|
||||
price DECIMAL(10,2) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
DROP PROCEDURE IF EXISTS seed_data$$
|
||||
CREATE PROCEDURE seed_data()
|
||||
BEGIN
|
||||
DECLARE i BIGINT DEFAULT 1;
|
||||
DECLARE j BIGINT;
|
||||
DECLARE large_text TEXT;
|
||||
|
||||
SET large_text = REPEAT('Lorem ipsum dolor sit amet, consectetur adipiscing elit. ', 300);
|
||||
|
||||
WHILE i <= 200000 DO
|
||||
INSERT INTO users (username, email, password)
|
||||
VALUES (CONCAT('user', i), CONCAT('user', i, '@example.com'), 'changeme');
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
|
||||
SET i = 1;
|
||||
WHILE i <= 200000 DO
|
||||
INSERT INTO products (name, description, price)
|
||||
VALUES (CONCAT('Product ', i), large_text, ROUND(RAND()*1000,2));
|
||||
SET i = i + 1;
|
||||
END WHILE;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
CALL seed_data();
|
||||
|
||||
DROP PROCEDURE IF EXISTS seed_data;
|
||||
@@ -60,7 +60,7 @@ SELECT
|
||||
)
|
||||
FROM users u
|
||||
JOIN generate_series(1, 30) AS p(post_no)
|
||||
ON u.id <= 300000;
|
||||
ON u.id <= 500000;
|
||||
|
||||
-- ============================================================
|
||||
-- OPTIONAL: FORCE DISK MATERIALIZATION
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Drop tables
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS projects;
|
||||
DROP TABLE IF EXISTS tasks;
|
||||
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
full_name TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin','manager','user')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- Projects
|
||||
CREATE TABLE projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
owner_id INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Tasks
|
||||
CREATE TABLE tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('todo','in_progress','done')),
|
||||
priority INTEGER NOT NULL DEFAULT 3,
|
||||
due_date TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Seed minimal users
|
||||
INSERT INTO users (email, full_name, role) VALUES
|
||||
('admin@example.com', 'System Admin', 'admin'),
|
||||
('manager@example.com', 'Project Manager', 'manager'),
|
||||
('user@example.com', 'Standard User', 'user');
|
||||
|
||||
-- Generate 10_000 projects
|
||||
WITH RECURSIVE numbers(x) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT x+1 FROM numbers WHERE x<10000
|
||||
)
|
||||
INSERT INTO projects (name, description, owner_id)
|
||||
SELECT
|
||||
'Project #' || x,
|
||||
'Auto-generated project description for project #' || x,
|
||||
(1 + (x % 3)) -- cycle users 1..3
|
||||
FROM numbers;
|
||||
|
||||
-- Generate 100_000 tasks
|
||||
WITH RECURSIVE numbers(x) AS (
|
||||
SELECT 1
|
||||
UNION ALL
|
||||
SELECT x+1 FROM numbers WHERE x<100000
|
||||
)
|
||||
INSERT INTO tasks (project_id, title, status, priority, due_date)
|
||||
SELECT
|
||||
(1 + (x % 10000)), -- project id cycle
|
||||
'Task #' || x,
|
||||
CASE (x % 3) WHEN 0 THEN 'todo' WHEN 1 THEN 'in_progress' ELSE 'done' END,
|
||||
1 + (x % 5),
|
||||
date('now', '+' || (x % 30) || ' days')
|
||||
FROM numbers;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,59 @@
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
-- Drop existing tables (idempotent reset)
|
||||
DROP TABLE IF EXISTS users;
|
||||
DROP TABLE IF EXISTS projects;
|
||||
DROP TABLE IF EXISTS tasks;
|
||||
|
||||
-- Users
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
full_name TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin','manager','user')),
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
-- Projects
|
||||
CREATE TABLE projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
owner_id INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Tasks
|
||||
CREATE TABLE tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
project_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL CHECK (status IN ('todo','in_progress','done')),
|
||||
priority INTEGER NOT NULL DEFAULT 3,
|
||||
due_date TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Seed users
|
||||
INSERT INTO users (email, full_name, role) VALUES
|
||||
('admin@example.com', 'System Admin', 'admin'),
|
||||
('manager@example.com', 'Project Manager', 'manager'),
|
||||
('user@example.com', 'Standard User', 'user');
|
||||
|
||||
-- Seed projects
|
||||
INSERT INTO projects (name, description, owner_id) VALUES
|
||||
('Internal Tooling', 'Backoffice automation platform', 2),
|
||||
('Client Portal', 'Customer-facing SaaS interface', 2);
|
||||
|
||||
-- Seed tasks
|
||||
INSERT INTO tasks (project_id, title, status, priority, due_date) VALUES
|
||||
(1, 'Define architecture', 'done', 1, date('now', '+3 days')),
|
||||
(1, 'Implement authentication', 'in_progress', 1, date('now', '+7 days')),
|
||||
(2, 'Design landing page', 'todo', 2, date('now', '+5 days')),
|
||||
(2, 'Setup CI/CD', 'todo', 2, date('now', '+10 days'));
|
||||
|
||||
COMMIT;
|
||||
Binary file not shown.
+16
-8
@@ -4,11 +4,11 @@ use crate::core::context::Context;
|
||||
use crate::services::backup::BackupService;
|
||||
use crate::services::config::ConfigService;
|
||||
use crate::services::cron::CronService;
|
||||
use crate::services::restore::RestoreService;
|
||||
use crate::services::status::StatusService;
|
||||
use crate::utils::common::BackupMethod;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
use crate::services::restore::RestoreService;
|
||||
|
||||
pub struct Agent {
|
||||
ctx: Arc<Context>,
|
||||
@@ -43,22 +43,30 @@ impl Agent {
|
||||
let ping_result = self.status_service.ping(&config.databases).await?;
|
||||
|
||||
for db in ping_result.databases.iter() {
|
||||
let database = config
|
||||
.databases
|
||||
.iter()
|
||||
.find(|cfg_db| cfg_db.generated_id == db.generated_id)
|
||||
.unwrap();
|
||||
info!(
|
||||
"Generated Id: {} | backup action: {} | restore action: {}",
|
||||
db.generated_id, db.data.backup.action, db.data.restore.action
|
||||
"Generated Id: {} | backup action: {} | restore action: {} | Database Name: {}",
|
||||
db.generated_id, db.data.backup.action, db.data.restore.action, database.name,
|
||||
);
|
||||
let _ = self.cron_service.sync(db).await;
|
||||
|
||||
if db.data.backup.action {
|
||||
let _ = self
|
||||
.backup_service
|
||||
.dispatch(&db.generated_id, &config, method.clone())
|
||||
.dispatch(
|
||||
&db.generated_id,
|
||||
&config,
|
||||
method.clone(),
|
||||
&db.storages,
|
||||
db.encrypt,
|
||||
)
|
||||
.await;
|
||||
} else if db.data.restore.action {
|
||||
let _ = self
|
||||
.restore_service
|
||||
.dispatch(db, &config)
|
||||
.await;
|
||||
let _ = self.restore_service.dispatch(db, &config).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -1,3 +1,4 @@
|
||||
use crate::services::api::ApiClient;
|
||||
use crate::settings::CONFIG;
|
||||
use crate::utils::edge_key::{EdgeKey, EdgeKeyError, decode_edge_key};
|
||||
use tracing::{debug, error, info};
|
||||
@@ -6,6 +7,7 @@ use tracing::{debug, error, info};
|
||||
pub struct Context {
|
||||
#[allow(dead_code)]
|
||||
pub edge_key: EdgeKey,
|
||||
pub api: ApiClient,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -34,6 +36,12 @@ impl Context {
|
||||
}
|
||||
};
|
||||
|
||||
Context { edge_key }
|
||||
let server_url = format!("{}/api", edge_key.server_url);
|
||||
let api_client = ApiClient::new(server_url);
|
||||
|
||||
Context {
|
||||
edge_key: edge_key,
|
||||
api: api_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
pub mod context;
|
||||
pub mod agent;
|
||||
pub mod context;
|
||||
|
||||
@@ -2,6 +2,9 @@ use crate::domain::mongodb::database::MongoDatabase;
|
||||
use crate::domain::mysql::database::MySQLDatabase;
|
||||
use crate::domain::postgres::database::PostgresDatabase;
|
||||
use crate::domain::postgres::{detect_format_from_file, detect_format_from_size};
|
||||
use crate::domain::redis::database::RedisDatabase;
|
||||
use crate::domain::sqlite::database::SqliteDatabase;
|
||||
use crate::domain::valkey::database::ValkeyDatabase;
|
||||
use crate::services::config::{DatabaseConfig, DbType};
|
||||
use anyhow::Result;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -27,6 +30,9 @@ impl DatabaseFactory {
|
||||
DbType::Mysql => Arc::new(MySQLDatabase::new(cfg)),
|
||||
DbType::Mariadb => Arc::new(MySQLDatabase::new(cfg)),
|
||||
DbType::MongoDB => Arc::new(MongoDatabase::new(cfg)),
|
||||
DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)),
|
||||
DbType::Redis => Arc::new(RedisDatabase::new(cfg)),
|
||||
DbType::Valkey => Arc::new(ValkeyDatabase::new(cfg)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +45,9 @@ impl DatabaseFactory {
|
||||
DbType::Mysql => Arc::new(MySQLDatabase::new(cfg)),
|
||||
DbType::Mariadb => Arc::new(MySQLDatabase::new(cfg)),
|
||||
DbType::MongoDB => Arc::new(MongoDatabase::new(cfg)),
|
||||
DbType::Sqlite => Arc::new(SqliteDatabase::new(cfg)),
|
||||
DbType::Redis => Arc::new(RedisDatabase::new(cfg)),
|
||||
DbType::Valkey => Arc::new(ValkeyDatabase::new(cfg)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,5 +1,7 @@
|
||||
pub mod factory;
|
||||
pub mod postgres;
|
||||
pub mod mysql;
|
||||
mod mongodb;
|
||||
|
||||
pub mod mysql;
|
||||
pub mod postgres;
|
||||
mod redis;
|
||||
mod sqlite;
|
||||
mod valkey;
|
||||
|
||||
@@ -15,7 +15,8 @@ pub async fn run(
|
||||
|
||||
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
|
||||
let mongodump = select_mongo_path().join("mongodump");
|
||||
let uri = get_mongo_uri(cfg.clone());
|
||||
info!("{:?}", mongodump);
|
||||
let uri = get_mongo_uri(cfg.clone())?;
|
||||
|
||||
let output = Command::new(mongodump)
|
||||
.arg(format!("--uri={}", uri))
|
||||
|
||||
@@ -3,7 +3,7 @@ use anyhow::Result;
|
||||
use mongodb::Client;
|
||||
|
||||
pub async fn connect(cfg: DatabaseConfig) -> Result<Client> {
|
||||
let uri = get_mongo_uri(cfg);
|
||||
let uri = get_mongo_uri(cfg)?;
|
||||
let mut options = mongodb::options::ClientOptions::parse(&uri).await?;
|
||||
options.server_selection_timeout = Some(std::time::Duration::from_secs(3));
|
||||
options.connect_timeout = Some(std::time::Duration::from_secs(3));
|
||||
@@ -15,14 +15,16 @@ pub fn select_mongo_path() -> std::path::PathBuf {
|
||||
"/usr/local/mongodb/bin".to_string().into()
|
||||
}
|
||||
|
||||
pub fn get_mongo_uri(cfg: DatabaseConfig) -> String {
|
||||
if cfg.username.is_empty() {
|
||||
format!("mongodb://{}:{}/{}", cfg.host, cfg.port, cfg.database)
|
||||
|
||||
pub fn get_mongo_uri(cfg: DatabaseConfig) -> Result<String> {
|
||||
if cfg.username.is_empty() || cfg.password.is_empty() {
|
||||
Ok(format!(
|
||||
"mongodb://{}:{}/{}",
|
||||
cfg.host, cfg.port, cfg.database
|
||||
))
|
||||
} else {
|
||||
format!(
|
||||
Ok(format!(
|
||||
"mongodb://{}:{}@{}:{}/{}?authSource=admin",
|
||||
cfg.username, cfg.password, cfg.host, cfg.port, cfg.database
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,7 @@ impl Database for MongoDatabase {
|
||||
|
||||
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
let res = backup::run(
|
||||
self.cfg.clone(),
|
||||
dir.to_path_buf(),
|
||||
self.file_extension(),
|
||||
)
|
||||
.await;
|
||||
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
mod backup;
|
||||
mod restore;
|
||||
mod connection;
|
||||
pub mod database;
|
||||
mod ping;
|
||||
mod connection;
|
||||
mod restore;
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::domain::mongodb::connection::connect;
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::Result;
|
||||
use mongodb::bson::doc;
|
||||
use tracing::{error};
|
||||
use crate::domain::mongodb::connection::connect;
|
||||
use tracing::error;
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
|
||||
let client = connect(cfg.clone()).await?;
|
||||
let db_name = if cfg.username.is_empty() { &cfg.database } else { "admin" };
|
||||
|
||||
let db_name = if cfg.username.is_empty() && cfg.password.is_empty() {
|
||||
&cfg.database
|
||||
} else {
|
||||
"admin"
|
||||
};
|
||||
|
||||
match client.database(db_name).run_command(doc! {"ping": 1}).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(e) => {
|
||||
@@ -18,7 +24,7 @@ pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
|
||||
error!("Full Error: {}", e);
|
||||
error!("Check you database network connectivity");
|
||||
error!("----------------------------------------");
|
||||
Err(anyhow::anyhow!("Ping failed for {}: {}", cfg.name, e))
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
|
||||
debug!("Starting MongoDB restore for database {}", cfg.name);
|
||||
|
||||
let mongorestore = select_mongo_path().join("mongorestore");
|
||||
let uri = get_mongo_uri(cfg.clone());
|
||||
let uri = get_mongo_uri(cfg.clone())?;
|
||||
|
||||
let output = Command::new(mongorestore)
|
||||
.arg(format!("--uri={}", uri))
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use std::process::Command;
|
||||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
pub async fn server_version(cfg: &DatabaseConfig) -> Result<String> {
|
||||
let output = Command::new("mysql")
|
||||
.arg("--host").arg(&cfg.host)
|
||||
.arg("--port").arg(cfg.port.to_string())
|
||||
.arg("--user").arg(&cfg.username)
|
||||
.arg("-e").arg("SELECT VERSION();")
|
||||
.arg("--host")
|
||||
.arg(&cfg.host)
|
||||
.arg("--port")
|
||||
.arg(cfg.port.to_string())
|
||||
.arg("--user")
|
||||
.arg(&cfg.username)
|
||||
.arg("-e")
|
||||
.arg("SELECT VERSION();")
|
||||
.env("MYSQL_PWD", &cfg.password)
|
||||
.output()?;
|
||||
|
||||
@@ -18,7 +22,7 @@ pub async fn server_version(cfg: &DatabaseConfig) -> Result<String> {
|
||||
|
||||
let version = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.nth(1) // skip column header
|
||||
.nth(1)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
use super::{
|
||||
backup,
|
||||
ping, restore,
|
||||
};
|
||||
use super::{backup, ping, restore};
|
||||
use crate::domain::factory::Database;
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use crate::utils::locks::{DbOpLock, FileLock};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub struct MySQLDatabase {
|
||||
cfg: DatabaseConfig,
|
||||
@@ -21,7 +18,7 @@ impl MySQLDatabase {
|
||||
|
||||
fn build_env(&self) -> HashMap<String, String> {
|
||||
let mut envs = std::env::vars().collect::<HashMap<_, _>>();
|
||||
envs.insert("MYSQL_PWD".to_string(), self.cfg.password.clone());
|
||||
envs.insert("MYSQL_PWD".to_string(), self.cfg.password.to_string());
|
||||
envs
|
||||
}
|
||||
}
|
||||
@@ -38,7 +35,13 @@ impl Database for MySQLDatabase {
|
||||
|
||||
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.build_env().clone(), self.file_extension()).await;
|
||||
let res = backup::run(
|
||||
self.cfg.clone(),
|
||||
dir.to_path_buf(),
|
||||
self.build_env().clone(),
|
||||
self.file_extension(),
|
||||
)
|
||||
.await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
res
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod backup;
|
||||
mod connection;
|
||||
pub mod database;
|
||||
mod restore;
|
||||
mod ping;
|
||||
mod connection;
|
||||
mod restore;
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::Context;
|
||||
use std::collections::HashMap;
|
||||
use std::process::Command;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig, env: HashMap<String, String>) -> anyhow::Result<bool> {
|
||||
let output = Command::new("mysqladmin")
|
||||
.arg("--host")
|
||||
let mut cmd = Command::new("mysqladmin");
|
||||
cmd.arg("--host")
|
||||
.arg(cfg.host)
|
||||
.arg("--port")
|
||||
.arg(cfg.port.to_string())
|
||||
.arg("--user")
|
||||
.arg(cfg.username)
|
||||
.arg("ping")
|
||||
.envs(env)
|
||||
.output()
|
||||
.with_context(|| format!("Failed to ping MySQL server {}", cfg.name))?;
|
||||
Ok(output.status.success())
|
||||
.envs(env);
|
||||
|
||||
let result = timeout(Duration::from_secs(10), cmd.output()).await;
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
let output = output?;
|
||||
Ok(output.status.success())
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{debug, error, info};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
|
||||
let handle = tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
@@ -55,7 +54,8 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
|
||||
.with_context(|| format!("Failed to start mysql restore for {}", cfg.name))?;
|
||||
|
||||
let mut stdin = child.stdin.take().context("Failed to open child stdin")?;
|
||||
stdin.write_all(sql_content.as_bytes())
|
||||
stdin
|
||||
.write_all(sql_content.as_bytes())
|
||||
.context("Failed to write SQL content to mysql stdin")?;
|
||||
stdin.flush()?;
|
||||
drop(stdin);
|
||||
@@ -74,8 +74,7 @@ pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
|
||||
Ok(())
|
||||
});
|
||||
|
||||
handle
|
||||
.await??;
|
||||
handle.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use tracing::{debug, error, info};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use super::connection::{select_pg_path, server_version};
|
||||
use super::format::PostgresDumpFormat;
|
||||
@@ -27,6 +27,7 @@ pub async fn run(
|
||||
};
|
||||
|
||||
let pg_dump = select_pg_path(&version).join("pg_dump");
|
||||
|
||||
debug!("Using pg_dump at {:?}", pg_dump);
|
||||
|
||||
match format {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::path::Path;
|
||||
use crate::domain::postgres::format::PostgresDumpFormat;
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use tokio_postgres::{Client, NoTls};
|
||||
use tracing::info;
|
||||
use tracing::{error, info};
|
||||
|
||||
pub async fn connect(cfg: &DatabaseConfig) -> Result<Client> {
|
||||
info!("Connecting to postgres database {}:{}", cfg.host, cfg.port);
|
||||
let dsn = format!(
|
||||
"host={} port={} user={} password={} dbname={}",
|
||||
cfg.host, cfg.port, cfg.username, cfg.password, cfg.database
|
||||
@@ -14,7 +15,7 @@ pub async fn connect(cfg: &DatabaseConfig) -> Result<Client> {
|
||||
let (client, connection) = tokio_postgres::connect(&dsn, NoTls).await?;
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
tracing::error!("Postgres connection error: {}", e);
|
||||
error!("Postgres connection error: {}", e);
|
||||
}
|
||||
});
|
||||
Ok(client)
|
||||
@@ -34,7 +35,7 @@ pub fn select_pg_path(version: &str) -> std::path::PathBuf {
|
||||
|
||||
pub async fn terminate_connections(cfg: &DatabaseConfig) -> Result<()> {
|
||||
let mut admin = cfg.clone();
|
||||
admin.database = "postgres".into();
|
||||
admin.database = "postgres".to_string().into();
|
||||
|
||||
let client = connect(&admin).await?;
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{
|
||||
backup,
|
||||
format::PostgresDumpFormat,
|
||||
ping, restore,
|
||||
};
|
||||
use super::{backup, format::PostgresDumpFormat, ping, restore};
|
||||
use crate::domain::factory::Database;
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use crate::utils::locks::{DbOpLock, FileLock};
|
||||
@@ -28,7 +24,6 @@ impl Database for PostgresDatabase {
|
||||
match self.format {
|
||||
PostgresDumpFormat::Fc => ".dump",
|
||||
PostgresDumpFormat::Fd => ".gz",
|
||||
// PostgresDumpFormat::Fd => ".tar.gz",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +35,6 @@ impl Database for PostgresDatabase {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
let res = backup::run(self.cfg.clone(), self.format, dir.to_path_buf()).await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
pub enum PostgresDumpFormat {
|
||||
Fc,
|
||||
Fd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod backup;
|
||||
pub mod database;
|
||||
mod restore;
|
||||
mod connection;
|
||||
pub mod database;
|
||||
mod format;
|
||||
mod ping;
|
||||
mod restore;
|
||||
|
||||
pub use connection::{detect_format_from_size, detect_format_from_file};
|
||||
pub use connection::{detect_format_from_file, detect_format_from_size};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use super::connection::connect;
|
||||
use crate::services::config::DatabaseConfig;
|
||||
|
||||
pub async fn run(
|
||||
cfg: DatabaseConfig,
|
||||
) -> anyhow::Result<bool> {
|
||||
pub async fn run(cfg: DatabaseConfig) -> anyhow::Result<bool> {
|
||||
Ok(connect(&cfg).await.is_ok())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use tracing::{debug, error, info};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
use super::connection::{select_pg_path, server_version, terminate_connections};
|
||||
use super::format::PostgresDumpFormat;
|
||||
@@ -14,6 +14,7 @@ pub async fn run(
|
||||
) -> Result<()> {
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
debug!("Starting restore for database {}", cfg.name);
|
||||
|
||||
let version = match futures::executor::block_on(server_version(&cfg)) {
|
||||
Ok(v) => {
|
||||
debug!("Postgres version detected: {}", v);
|
||||
@@ -26,6 +27,7 @@ pub async fn run(
|
||||
};
|
||||
|
||||
let pg_restore = select_pg_path(&version).join("pg_restore");
|
||||
|
||||
debug!("Using pg_restore at {:?}", pg_restore);
|
||||
|
||||
if let Err(e) = futures::executor::block_on(terminate_connections(&cfg)) {
|
||||
@@ -86,6 +88,8 @@ pub async fn run(
|
||||
}
|
||||
};
|
||||
|
||||
info!("tar_gz {:?}", tar_gz);
|
||||
|
||||
let dec = flate2::read::GzDecoder::new(tar_gz);
|
||||
let mut archive = tar::Archive::new(dec);
|
||||
|
||||
@@ -106,6 +110,7 @@ pub async fn run(
|
||||
}
|
||||
|
||||
debug!("Listing contents of temp dir: {}", tmp_dir.path().display());
|
||||
|
||||
for entry in std::fs::read_dir(tmp_dir.path())? {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub async fn run(
|
||||
cfg: DatabaseConfig,
|
||||
backup_dir: PathBuf,
|
||||
file_extension: &'static str,
|
||||
) -> Result<PathBuf> {
|
||||
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
|
||||
debug!("Starting Redis backup for database {}", cfg.name);
|
||||
|
||||
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
|
||||
|
||||
let mut cmd = Command::new("redis-cli");
|
||||
|
||||
cmd.arg("-h")
|
||||
.arg(&cfg.host)
|
||||
.arg("-p")
|
||||
.arg(cfg.port.to_string());
|
||||
|
||||
if !cfg.username.is_empty() {
|
||||
cmd.arg("--user").arg(&cfg.username);
|
||||
}
|
||||
|
||||
if !cfg.password.is_empty() {
|
||||
cmd.arg("-a").arg(&cfg.password);
|
||||
}
|
||||
|
||||
cmd.arg("--rdb").arg(&file_path);
|
||||
|
||||
debug!("Command Backup: {:?}", cmd);
|
||||
|
||||
let output = cmd.output().context("Redis backup command failed")?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !output.status.success() {
|
||||
if stderr.contains("NOAUTH") {
|
||||
error!(
|
||||
"Redis backup failed for {}: Authentication required (NOAUTH)",
|
||||
cfg.name
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Redis backup failed for {}: Authentication required",
|
||||
cfg.name
|
||||
);
|
||||
} else {
|
||||
error!("Redis backup failed for {}: {}", cfg.name, stderr);
|
||||
anyhow::bail!("Redis backup failed for {}: {}", cfg.name, stderr);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Redis backup completed for {}. Output: {}",
|
||||
cfg.name, stdout
|
||||
);
|
||||
|
||||
Ok(file_path)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::domain::factory::Database;
|
||||
use crate::domain::redis::{backup, ping};
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use crate::utils::locks::{DbOpLock, FileLock};
|
||||
|
||||
pub struct RedisDatabase {
|
||||
cfg: DatabaseConfig,
|
||||
}
|
||||
|
||||
impl RedisDatabase {
|
||||
pub fn new(cfg: DatabaseConfig) -> Self {
|
||||
Self { cfg }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Database for RedisDatabase {
|
||||
fn file_extension(&self) -> &'static str {
|
||||
".rdb"
|
||||
}
|
||||
|
||||
async fn ping(&self) -> Result<bool> {
|
||||
ping::run(self.cfg.clone()).await
|
||||
}
|
||||
|
||||
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
res
|
||||
}
|
||||
|
||||
async fn restore(&self, _file: &Path) -> Result<()> {
|
||||
bail!("Restore not supported for Redis databases")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod backup;
|
||||
pub mod database;
|
||||
mod ping;
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
|
||||
let mut cmd = Command::new("redis-cli");
|
||||
cmd.arg("-h")
|
||||
.arg(&cfg.host)
|
||||
.arg("-p")
|
||||
.arg(cfg.port.to_string());
|
||||
|
||||
if !cfg.username.is_empty() {
|
||||
cmd.arg("--user").arg(&cfg.username);
|
||||
}
|
||||
|
||||
if !cfg.password.is_empty() {
|
||||
cmd.arg("-a").arg(&cfg.password);
|
||||
}
|
||||
|
||||
cmd.arg("PING");
|
||||
|
||||
debug!("Command Ping: {:?}", cmd);
|
||||
|
||||
let result = timeout(Duration::from_secs(10), cmd.output()).await;
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
let output = output.context("Failed to execute redis-cli")?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
info!("Redis stdout: {}", stdout);
|
||||
info!("Redis stderr: {}", stderr);
|
||||
|
||||
if stderr.contains("NOAUTH") {
|
||||
info!("Redis authentication failed (NOAUTH required)");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
info!("Redis command failed with status: {:?}", output.status);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(stdout.contains("PONG"))
|
||||
}
|
||||
Err(_) => {
|
||||
info!("Timeout connecting to Redis at {}:{}", cfg.host, cfg.port);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub async fn run(
|
||||
cfg: DatabaseConfig,
|
||||
backup_dir: PathBuf,
|
||||
file_extension: &'static str,
|
||||
) -> Result<PathBuf> {
|
||||
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
|
||||
debug!("Starting SQLite backup for database {}", cfg.name);
|
||||
|
||||
let db_path_str = if cfg.path.is_empty() {
|
||||
anyhow::bail!("Database path not configured");
|
||||
} else {
|
||||
cfg.path.as_str().to_string()
|
||||
};
|
||||
|
||||
let db_path = PathBuf::from(db_path_str);
|
||||
|
||||
if !db_path.exists() {
|
||||
anyhow::bail!("SQLite database file not found: {}", db_path.display());
|
||||
}
|
||||
|
||||
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
|
||||
|
||||
let output = Command::new("sqlite3")
|
||||
.arg(db_path.as_os_str())
|
||||
.arg(format!(".backup '{}'", file_path.display()))
|
||||
.output()
|
||||
.context("SQLite backup command failed to start")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!("SQLite backup failed for {}: {}", cfg.name, stderr);
|
||||
anyhow::bail!("SQLite backup failed for {}: {}", cfg.name, stderr);
|
||||
}
|
||||
|
||||
info!("SQLite backup completed for {}", cfg.name);
|
||||
Ok(file_path)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{backup, ping, restore};
|
||||
use crate::domain::factory::Database;
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use crate::utils::locks::{DbOpLock, FileLock};
|
||||
|
||||
pub struct SqliteDatabase {
|
||||
cfg: DatabaseConfig,
|
||||
}
|
||||
|
||||
impl SqliteDatabase {
|
||||
pub fn new(cfg: DatabaseConfig) -> Self {
|
||||
Self { cfg }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Database for SqliteDatabase {
|
||||
fn file_extension(&self) -> &'static str {
|
||||
".backup"
|
||||
}
|
||||
|
||||
async fn ping(&self) -> Result<bool> {
|
||||
ping::run(self.cfg.clone()).await
|
||||
}
|
||||
|
||||
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
res
|
||||
}
|
||||
async fn restore(&self, file: &Path) -> Result<()> {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Restore.as_str()).await?;
|
||||
let res = restore::run(self.cfg.clone(), file.to_path_buf()).await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod backup;
|
||||
pub mod database;
|
||||
mod ping;
|
||||
mod restore;
|
||||
@@ -0,0 +1,5 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
|
||||
pub async fn run(_cfg: DatabaseConfig) -> anyhow::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig, restore_file: PathBuf) -> Result<()> {
|
||||
tokio::task::spawn_blocking(move || -> Result<()> {
|
||||
debug!("Starting SQLite restore for database {}", cfg.name);
|
||||
|
||||
let db_path_str = if cfg.path.is_empty() {
|
||||
anyhow::bail!("Database path not configured");
|
||||
} else {
|
||||
cfg.path.as_str().to_string()
|
||||
};
|
||||
|
||||
let db_path = PathBuf::from(db_path_str);
|
||||
|
||||
if !restore_file.exists() {
|
||||
anyhow::bail!("Restore file not found: {}", restore_file.display());
|
||||
}
|
||||
|
||||
if db_path.exists() {
|
||||
std::fs::remove_file(&db_path)
|
||||
.with_context(|| format!("Failed to remove existing DB {}", db_path.display()))?;
|
||||
}
|
||||
|
||||
let output = Command::new("sqlite3")
|
||||
.arg(db_path.as_os_str())
|
||||
.arg(format!(".restore '{}'", restore_file.display()))
|
||||
.output()
|
||||
.with_context(|| format!("Failed to run sqlite3 restore for {}", cfg.name))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
error!("SQLite restore failed for {}: {}", cfg.name, stderr);
|
||||
anyhow::bail!("SQLite restore failed for {}", cfg.name);
|
||||
}
|
||||
|
||||
info!("SQLite restore completed for {}", cfg.name);
|
||||
Ok(())
|
||||
})
|
||||
.await?
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
pub async fn run(
|
||||
cfg: DatabaseConfig,
|
||||
backup_dir: PathBuf,
|
||||
file_extension: &'static str,
|
||||
) -> Result<PathBuf> {
|
||||
tokio::task::spawn_blocking(move || -> Result<PathBuf> {
|
||||
debug!("Starting Valkey backup for database {}", cfg.name);
|
||||
|
||||
let file_path = backup_dir.join(format!("{}{}", cfg.generated_id, file_extension));
|
||||
|
||||
let mut cmd = Command::new("valkey-cli");
|
||||
|
||||
cmd.arg("-h")
|
||||
.arg(&cfg.host)
|
||||
.arg("-p")
|
||||
.arg(cfg.port.to_string());
|
||||
|
||||
if !cfg.username.is_empty() {
|
||||
cmd.arg("--user").arg(&cfg.username);
|
||||
}
|
||||
|
||||
if !cfg.password.is_empty() {
|
||||
cmd.arg("-a").arg(&cfg.password);
|
||||
}
|
||||
|
||||
cmd.arg("--rdb").arg(&file_path);
|
||||
|
||||
debug!("Command Backup: {:?}", cmd);
|
||||
|
||||
let output = cmd.output().context("Valkey backup command failed")?;
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !output.status.success() {
|
||||
if stderr.contains("NOAUTH") {
|
||||
error!(
|
||||
"Valkey backup failed for {}: Authentication required (NOAUTH)",
|
||||
cfg.name
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Valkey backup failed for {}: Authentication required",
|
||||
cfg.name
|
||||
);
|
||||
} else {
|
||||
error!("Valkey backup failed for {}: {}", cfg.name, stderr);
|
||||
anyhow::bail!("Valkey backup failed for {}: {}", cfg.name, stderr);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
"Valkey backup completed for {}. Output: {}",
|
||||
cfg.name, stdout
|
||||
);
|
||||
|
||||
Ok(file_path)
|
||||
})
|
||||
.await?
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::domain::factory::Database;
|
||||
use crate::domain::valkey::{backup, ping};
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use crate::utils::locks::{DbOpLock, FileLock};
|
||||
|
||||
pub struct ValkeyDatabase {
|
||||
cfg: DatabaseConfig,
|
||||
}
|
||||
|
||||
impl ValkeyDatabase {
|
||||
pub fn new(cfg: DatabaseConfig) -> Self {
|
||||
Self { cfg }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Database for ValkeyDatabase {
|
||||
fn file_extension(&self) -> &'static str {
|
||||
".rdb"
|
||||
}
|
||||
|
||||
async fn ping(&self) -> Result<bool> {
|
||||
ping::run(self.cfg.clone()).await
|
||||
}
|
||||
|
||||
async fn backup(&self, dir: &Path) -> Result<PathBuf> {
|
||||
FileLock::acquire(&self.cfg.generated_id, DbOpLock::Backup.as_str()).await?;
|
||||
let res = backup::run(self.cfg.clone(), dir.to_path_buf(), self.file_extension()).await;
|
||||
FileLock::release(&self.cfg.generated_id).await?;
|
||||
res
|
||||
}
|
||||
|
||||
async fn restore(&self, _file: &Path) -> Result<()> {
|
||||
bail!("Restore not supported for Valkey databases")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod backup;
|
||||
pub mod database;
|
||||
mod ping;
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::services::config::DatabaseConfig;
|
||||
use anyhow::{Context, Result};
|
||||
use tokio::process::Command;
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub async fn run(cfg: DatabaseConfig) -> Result<bool> {
|
||||
let mut cmd = Command::new("valkey-cli");
|
||||
cmd.arg("-h")
|
||||
.arg(&cfg.host)
|
||||
.arg("-p")
|
||||
.arg(cfg.port.to_string());
|
||||
|
||||
if !cfg.username.is_empty() {
|
||||
cmd.arg("--user").arg(&cfg.username);
|
||||
}
|
||||
|
||||
if !cfg.password.is_empty() {
|
||||
cmd.arg("-a").arg(&cfg.password);
|
||||
}
|
||||
|
||||
cmd.arg("PING");
|
||||
|
||||
debug!("Command Ping: {:?}", cmd);
|
||||
|
||||
let result = timeout(Duration::from_secs(10), cmd.output()).await;
|
||||
|
||||
match result {
|
||||
Ok(output) => {
|
||||
let output = output.context("Failed to execute redis-cli")?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
|
||||
info!("Redis stdout: {}", stdout);
|
||||
info!("Redis stderr: {}", stderr);
|
||||
|
||||
if stderr.contains("NOAUTH") {
|
||||
info!("Redis authentication failed (NOAUTH required)");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !output.status.success() {
|
||||
info!("Redis command failed with status: {:?}", output.status);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
Ok(stdout.contains("PONG"))
|
||||
}
|
||||
Err(_) => {
|
||||
info!("Timeout connecting to Redis at {}:{}", cfg.host, cfg.port);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -3,17 +3,18 @@ mod domain;
|
||||
mod services;
|
||||
mod settings;
|
||||
mod tasks;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
use crate::tasks::ping::ping_server;
|
||||
use crate::utils::locks::FileLock;
|
||||
use crate::utils::logging;
|
||||
use utils::redis_client;
|
||||
use utils::task_manager::scheduler;
|
||||
use crate::utils::logging;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
||||
logging::init_logger();
|
||||
|
||||
// Remove all locks on startup
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::services::api::ApiError;
|
||||
use reqwest::{Client, Method};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiClient {
|
||||
base_url: String,
|
||||
http: Client,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub fn new(base_url: impl Into<String>) -> Self {
|
||||
let http = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()
|
||||
.expect("failed to build http client");
|
||||
|
||||
Self {
|
||||
base_url: base_url.into(),
|
||||
http,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request<T: DeserializeOwned>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
) -> Result<Option<T>, ApiError> {
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
|
||||
let res = self.http.request(method, &url).send().await?;
|
||||
let status = res.status();
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(ApiError::HttpResponse { status, body });
|
||||
}
|
||||
|
||||
if body.trim().is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(serde_json::from_str::<T>(&body)?))
|
||||
}
|
||||
}
|
||||
pub async fn request_with_body<T, B>(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
body: &B,
|
||||
) -> Result<Option<T>, ApiError>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
B: serde::Serialize,
|
||||
{
|
||||
let url = format!("{}{}", self.base_url, path);
|
||||
|
||||
let res = self.http.request(method, &url).json(body).send().await?;
|
||||
|
||||
let status = res.status();
|
||||
let body_text = res.text().await.unwrap_or_default();
|
||||
|
||||
if !status.is_success() {
|
||||
return Err(ApiError::HttpResponse {
|
||||
status,
|
||||
body: body_text,
|
||||
});
|
||||
}
|
||||
|
||||
if body_text.trim().is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(serde_json::from_str::<T>(&body_text)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
pub mod upload;
|
||||
|
||||
use crate::services::api::models::agent::backup::BackupResponse;
|
||||
use crate::services::api::{ApiClient, ApiError};
|
||||
use anyhow::Result;
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BackupCreateRequest {
|
||||
pub method: String,
|
||||
#[serde(rename = "generatedId")]
|
||||
pub generated_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct BackupUpdateRequest {
|
||||
#[serde(rename = "backupId")]
|
||||
pub backup_id: String,
|
||||
pub status: String,
|
||||
pub size: Option<u64>,
|
||||
#[serde(rename = "generatedId")]
|
||||
pub generated_id: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn backup_create(
|
||||
&self,
|
||||
method: impl Into<String>,
|
||||
agent_id: impl Into<String>,
|
||||
generated_id: impl Into<String>,
|
||||
) -> Result<Option<BackupResponse>, ApiError> {
|
||||
let body = BackupCreateRequest {
|
||||
method: method.into(),
|
||||
generated_id: generated_id.into(),
|
||||
};
|
||||
|
||||
let agent_id = agent_id.into();
|
||||
let path = format!("/agent/{}/backup", agent_id);
|
||||
|
||||
self.request_with_body(Method::POST, path.as_str(), &body)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn backup_update(
|
||||
&self,
|
||||
agent_id: impl Into<String>,
|
||||
backup_id: impl Into<String>,
|
||||
status: impl Into<String>,
|
||||
file_size: impl Into<Option<u64>>,
|
||||
generated_id: impl Into<String>,
|
||||
) -> Result<Option<BackupResponse>, ApiError> {
|
||||
let body = BackupUpdateRequest {
|
||||
backup_id: backup_id.into(),
|
||||
status: status.into(),
|
||||
size: file_size.into(),
|
||||
generated_id: generated_id.into(),
|
||||
};
|
||||
|
||||
let agent_id = agent_id.into();
|
||||
let path = format!("/agent/{}/backup", agent_id);
|
||||
|
||||
self.request_with_body(Method::PATCH, path.as_str(), &body)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use crate::services::api::models::agent::backup::BackupUploadResponse;
|
||||
use crate::services::api::{ApiClient, ApiError};
|
||||
use anyhow::Result;
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct InitUploadRequest {
|
||||
#[serde(rename = "generatedId")]
|
||||
pub generated_id: String,
|
||||
#[serde(rename = "storageChannelId")]
|
||||
pub storage_channel_id: String,
|
||||
#[serde(rename = "backupId")]
|
||||
pub backup_id: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn backup_upload_init(
|
||||
&self,
|
||||
agent_id: impl Into<String>,
|
||||
generated_id: impl Into<String>,
|
||||
storage_channel_id: impl Into<String>,
|
||||
backup_id: impl Into<String>,
|
||||
) -> Result<Option<BackupUploadResponse>, ApiError> {
|
||||
let body = InitUploadRequest {
|
||||
generated_id: generated_id.into(),
|
||||
storage_channel_id: storage_channel_id.into(),
|
||||
backup_id: backup_id.into(),
|
||||
};
|
||||
|
||||
let agent_id = agent_id.into();
|
||||
let path = format!("/agent/{}/backup/upload/init", agent_id);
|
||||
|
||||
self.request_with_body(Method::POST, path.as_str(), &body)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod init;
|
||||
pub mod status;
|
||||
@@ -0,0 +1,46 @@
|
||||
use crate::services::api::models::agent::backup::BackupUploadResponse;
|
||||
use crate::services::api::{ApiClient, ApiError};
|
||||
use anyhow::Result;
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StatusUploadRequest {
|
||||
#[serde(rename = "generatedId")]
|
||||
pub generated_id: String,
|
||||
#[serde(rename = "backupStorageId")]
|
||||
pub backup_storage_id: String,
|
||||
pub status: String,
|
||||
pub path: String,
|
||||
pub size: u64,
|
||||
#[serde(rename = "backupId")]
|
||||
pub backup_id: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn backup_upload_status(
|
||||
&self,
|
||||
agent_id: impl Into<String>,
|
||||
generated_id: impl Into<String>,
|
||||
backup_storage_id: impl Into<String>,
|
||||
status: impl Into<String>,
|
||||
remote_path: impl Into<String>,
|
||||
total_size: impl Into<u64>,
|
||||
backup_id: impl Into<String>,
|
||||
) -> Result<Option<BackupUploadResponse>, ApiError> {
|
||||
let body = StatusUploadRequest {
|
||||
generated_id: generated_id.into(),
|
||||
backup_storage_id: backup_storage_id.into(),
|
||||
status: status.into(),
|
||||
path: remote_path.into(),
|
||||
size: total_size.into(),
|
||||
backup_id: backup_id.into(),
|
||||
};
|
||||
|
||||
let agent_id = agent_id.into();
|
||||
let path = format!("/agent/{}/backup/upload/status", agent_id);
|
||||
|
||||
self.request_with_body(Method::PATCH, path.as_str(), &body)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod backup;
|
||||
pub mod restore;
|
||||
pub mod status;
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::services::api::models::agent::restore::ResultRestoreResponse;
|
||||
use crate::services::api::{ApiClient, ApiError};
|
||||
use anyhow::Result;
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ResultRestoreRequest {
|
||||
#[serde(rename = "generatedId")]
|
||||
pub generated_id: String,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn restore_result(
|
||||
&self,
|
||||
agent_id: impl Into<String>,
|
||||
generated_id: impl Into<String>,
|
||||
status: impl Into<String>,
|
||||
) -> Result<Option<ResultRestoreResponse>, ApiError> {
|
||||
let body = ResultRestoreRequest {
|
||||
generated_id: generated_id.into(),
|
||||
status: status.into(),
|
||||
};
|
||||
|
||||
let agent_id = agent_id.into();
|
||||
|
||||
let path = format!("/agent/{}/restore", agent_id);
|
||||
|
||||
self.request_with_body(Method::POST, path.as_str(), &body)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use crate::services::api::models::agent;
|
||||
use crate::services::api::{ApiClient, ApiError};
|
||||
use agent::status::PingResult;
|
||||
use anyhow::Result;
|
||||
use reqwest::Method;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DatabasePayload<'a> {
|
||||
pub name: &'a str,
|
||||
pub dbms: &'a str,
|
||||
#[serde(rename = "generatedId")]
|
||||
pub generated_id: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct StatusRequest<'a> {
|
||||
pub version: &'a str,
|
||||
pub databases: Vec<DatabasePayload<'a>>,
|
||||
}
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn agent_status<'a>(
|
||||
&self,
|
||||
agent_id: impl Into<String>,
|
||||
version: &'a str,
|
||||
databases: Vec<DatabasePayload<'a>>,
|
||||
) -> Result<Option<PingResult>, ApiError> {
|
||||
let body = StatusRequest { version, databases };
|
||||
|
||||
let agent_id = agent_id.into();
|
||||
let path = format!("/agent/{}/status", agent_id);
|
||||
|
||||
self.request_with_body(Method::POST, path.as_str(), &body)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod agent;
|
||||
|
||||
pub use agent::status;
|
||||
@@ -0,0 +1,18 @@
|
||||
use reqwest::StatusCode;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[allow(dead_code)]
|
||||
pub enum ApiError {
|
||||
#[error("http client error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("api error: status={status}, body={body}")]
|
||||
HttpResponse { status: StatusCode, body: String },
|
||||
|
||||
#[error("api returned unexpected response")]
|
||||
UnexpectedResponse,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod client;
|
||||
pub mod endpoints;
|
||||
pub mod error;
|
||||
pub mod models;
|
||||
|
||||
pub use client::ApiClient;
|
||||
pub use error::ApiError;
|
||||
@@ -0,0 +1,24 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct BackupStorage {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BackupUploadResponse {
|
||||
pub message: String,
|
||||
#[serde(rename = "backupStorage")]
|
||||
pub backup_storage: BackupStorage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Backup {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BackupResponse {
|
||||
pub message: String,
|
||||
pub backup: Backup,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod backup;
|
||||
pub mod restore;
|
||||
pub mod status;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user