mirror of
https://github.com/databasus/databasus.git
synced 2026-04-06 00:32:03 +02:00
FEATURE (agent): Setup agent directory, pre-commit and CI\CD workflow
This commit is contained in:
60
.github/workflows/ci-release.yml
vendored
60
.github/workflows/ci-release.yml
vendored
@@ -86,6 +86,39 @@ jobs:
|
|||||||
cd frontend
|
cd frontend
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
|
lint-agent:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.24.9"
|
||||||
|
cache-dependency-path: agent/go.sum
|
||||||
|
|
||||||
|
- name: Download Go modules
|
||||||
|
run: |
|
||||||
|
cd agent
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
- name: Install golangci-lint
|
||||||
|
run: |
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2
|
||||||
|
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
run: |
|
||||||
|
cd agent
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
- name: Verify go mod tidy
|
||||||
|
run: |
|
||||||
|
cd agent
|
||||||
|
go mod tidy
|
||||||
|
git diff --exit-code go.mod go.sum || (echo "go mod tidy made changes, please run 'go mod tidy' and commit the changes" && exit 1)
|
||||||
|
|
||||||
test-frontend:
|
test-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [lint-frontend]
|
needs: [lint-frontend]
|
||||||
@@ -108,6 +141,29 @@ jobs:
|
|||||||
cd frontend
|
cd frontend
|
||||||
npm run test
|
npm run test
|
||||||
|
|
||||||
|
test-agent:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint-agent]
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.24.9"
|
||||||
|
cache-dependency-path: agent/go.sum
|
||||||
|
|
||||||
|
- name: Download Go modules
|
||||||
|
run: |
|
||||||
|
cd agent
|
||||||
|
go mod download
|
||||||
|
|
||||||
|
- name: Run Go tests
|
||||||
|
run: |
|
||||||
|
cd agent
|
||||||
|
go test -count=1 -failfast ./internal/...
|
||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
needs: [lint-backend]
|
needs: [lint-backend]
|
||||||
@@ -441,7 +497,7 @@ jobs:
|
|||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
container:
|
container:
|
||||||
image: node:20
|
image: node:20
|
||||||
needs: [test-backend, test-frontend]
|
needs: [test-backend, test-frontend, test-agent]
|
||||||
if: ${{ github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip-release]') }}
|
if: ${{ github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip-release]') }}
|
||||||
outputs:
|
outputs:
|
||||||
should_release: ${{ steps.version_bump.outputs.should_release }}
|
should_release: ${{ steps.version_bump.outputs.should_release }}
|
||||||
@@ -534,7 +590,7 @@ jobs:
|
|||||||
|
|
||||||
build-only:
|
build-only:
|
||||||
runs-on: self-hosted
|
runs-on: self-hosted
|
||||||
needs: [test-backend, test-frontend]
|
needs: [test-backend, test-frontend, test-agent]
|
||||||
if: ${{ github.ref == 'refs/heads/main' && contains(github.event.head_commit.message, '[skip-release]') }}
|
if: ${{ github.ref == 'refs/heads/main' && contains(github.event.head_commit.message, '[skip-release]') }}
|
||||||
steps:
|
steps:
|
||||||
- name: Clean workspace
|
- name: Clean workspace
|
||||||
|
|||||||
@@ -41,3 +41,20 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
files: ^backend/.*\.go$
|
files: ^backend/.*\.go$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
||||||
|
# Agent checks
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: agent-format-and-lint
|
||||||
|
name: Agent Format & Lint (golangci-lint)
|
||||||
|
entry: bash -c "cd agent && golangci-lint fmt ./internal/... ./cmd/... && golangci-lint run ./internal/... ./cmd/..."
|
||||||
|
language: system
|
||||||
|
files: ^agent/.*\.go$
|
||||||
|
pass_filenames: false
|
||||||
|
|
||||||
|
- id: agent-go-mod-tidy
|
||||||
|
name: Agent Go Mod Tidy
|
||||||
|
entry: bash -c "cd agent && go mod tidy"
|
||||||
|
language: system
|
||||||
|
files: ^agent/.*\.go$
|
||||||
|
pass_filenames: false
|
||||||
|
|||||||
43
Dockerfile
43
Dockerfile
@@ -66,6 +66,43 @@ RUN CGO_ENABLED=0 \
|
|||||||
go build -o /app/main ./cmd/main.go
|
go build -o /app/main ./cmd/main.go
|
||||||
|
|
||||||
|
|
||||||
|
# ========= BUILD AGENT =========
|
||||||
|
# Builds the databasus-agent CLI binary for BOTH x86_64 and ARM64.
|
||||||
|
# Both architectures are always built because:
|
||||||
|
# - Databasus server runs on one arch (e.g. amd64)
|
||||||
|
# - The agent runs on remote PostgreSQL servers that may be on a
|
||||||
|
# different arch (e.g. arm64)
|
||||||
|
# - The backend serves the correct binary based on the agent's
|
||||||
|
# ?arch= query parameter
|
||||||
|
#
|
||||||
|
# We cross-compile from the build platform (no QEMU needed) because the
|
||||||
|
# agent is pure Go with zero C dependencies.
|
||||||
|
# CGO_ENABLED=0 produces fully static binaries — no glibc/musl dependency,
|
||||||
|
# so the agent runs on any Linux distro (Alpine, Debian, Ubuntu, RHEL, etc.).
|
||||||
|
# APP_VERSION is baked into the binary via -ldflags so the agent can
|
||||||
|
# compare its version against the server and auto-update when needed.
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:1.24.9 AS agent-build
|
||||||
|
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
|
||||||
|
WORKDIR /agent
|
||||||
|
|
||||||
|
COPY agent/go.mod ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY agent/ ./
|
||||||
|
|
||||||
|
# Build for x86_64 (amd64) — static binary, no glibc dependency
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -ldflags "-X main.Version=${APP_VERSION}" \
|
||||||
|
-o /agent-binaries/databasus-agent-linux-amd64 ./cmd/main.go
|
||||||
|
|
||||||
|
# Build for ARM64 (arm64) — static binary, no glibc dependency
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
|
||||||
|
go build -ldflags "-X main.Version=${APP_VERSION}" \
|
||||||
|
-o /agent-binaries/databasus-agent-linux-arm64 ./cmd/main.go
|
||||||
|
|
||||||
|
|
||||||
# ========= RUNTIME =========
|
# ========= RUNTIME =========
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -220,6 +257,10 @@ COPY backend/migrations ./migrations
|
|||||||
# Copy UI files
|
# Copy UI files
|
||||||
COPY --from=backend-build /app/ui/build ./ui/build
|
COPY --from=backend-build /app/ui/build ./ui/build
|
||||||
|
|
||||||
|
# Copy agent binaries (both architectures) — served by the backend
|
||||||
|
# at GET /api/v1/system/agent?arch=amd64|arm64
|
||||||
|
COPY --from=agent-build /agent-binaries ./agent-binaries
|
||||||
|
|
||||||
# Copy .env file (with fallback to .env.production.example)
|
# Copy .env file (with fallback to .env.production.example)
|
||||||
COPY backend/.env* /app/
|
COPY backend/.env* /app/
|
||||||
RUN if [ ! -f /app/.env ]; then \
|
RUN if [ ! -f /app/.env ]; then \
|
||||||
@@ -397,6 +438,8 @@ fi
|
|||||||
# Create database and set password for postgres user
|
# Create database and set password for postgres user
|
||||||
echo "Setting up database and user..."
|
echo "Setting up database and user..."
|
||||||
gosu postgres \$PG_BIN/psql -p 5437 -h localhost -d postgres << 'SQL'
|
gosu postgres \$PG_BIN/psql -p 5437 -h localhost -d postgres << 'SQL'
|
||||||
|
|
||||||
|
# We use stub password, because internal DB is not exposed outside container
|
||||||
ALTER USER postgres WITH PASSWORD 'Q1234567';
|
ALTER USER postgres WITH PASSWORD 'Q1234567';
|
||||||
SELECT 'CREATE DATABASE databasus OWNER postgres'
|
SELECT 'CREATE DATABASE databasus OWNER postgres'
|
||||||
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'databasus')
|
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'databasus')
|
||||||
|
|||||||
1
agent/.env.example
Normal file
1
agent/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ENV_MODE=development
|
||||||
23
agent/.gitignore
vendored
Normal file
23
agent/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
main
|
||||||
|
.env
|
||||||
|
docker-compose.yml
|
||||||
|
pgdata
|
||||||
|
pgdata_test/
|
||||||
|
mysqldata/
|
||||||
|
mariadbdata/
|
||||||
|
main.exe
|
||||||
|
swagger/
|
||||||
|
swagger/*
|
||||||
|
swagger/docs.go
|
||||||
|
swagger/swagger.json
|
||||||
|
swagger/swagger.yaml
|
||||||
|
postgresus-backend.exe
|
||||||
|
databasus-backend.exe
|
||||||
|
ui/build/*
|
||||||
|
pgdata-for-restore/
|
||||||
|
temp/
|
||||||
|
cmd.exe
|
||||||
|
temp/
|
||||||
|
valkey-data/
|
||||||
|
victoria-logs-data/
|
||||||
|
databasus.json
|
||||||
19
agent/.golangci.yml
Normal file
19
agent/.golangci.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
tests: false
|
||||||
|
concurrency: 4
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: standard
|
||||||
|
|
||||||
|
settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- golines
|
||||||
|
- goimports
|
||||||
12
agent/Makefile
Normal file
12
agent/Makefile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Usage: make run ARGS="start --pg-host localhost"
|
||||||
|
run:
|
||||||
|
go run cmd/main.go $(ARGS)
|
||||||
|
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build -ldflags "-X main.Version=$(VERSION)" -o databasus-agent ./cmd/main.go
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -count=1 -failfast ./internal/...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golangci-lint fmt ./cmd/... ./internal/... && golangci-lint run ./cmd/... ./internal/...
|
||||||
173
agent/cmd/main.go
Normal file
173
agent/cmd/main.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"databasus-agent/internal/config"
|
||||||
|
"databasus-agent/internal/features/start"
|
||||||
|
"databasus-agent/internal/features/upgrade"
|
||||||
|
"databasus-agent/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "start":
|
||||||
|
runStart(os.Args[2:])
|
||||||
|
case "stop":
|
||||||
|
runStop()
|
||||||
|
case "status":
|
||||||
|
runStatus()
|
||||||
|
case "restore":
|
||||||
|
runRestore(os.Args[2:])
|
||||||
|
case "version":
|
||||||
|
fmt.Println(Version)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStart(args []string) {
|
||||||
|
fs := flag.NewFlagSet("start", flag.ExitOnError)
|
||||||
|
|
||||||
|
isDebug := fs.Bool("debug", false, "Enable debug logging")
|
||||||
|
isSkipUpdate := fs.Bool("skip-update", false, "Skip auto-update check")
|
||||||
|
|
||||||
|
cfg := &config.Config{}
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, args)
|
||||||
|
|
||||||
|
if err := cfg.SaveToJSON(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to save config: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Init(*isDebug)
|
||||||
|
log := logger.GetLogger()
|
||||||
|
|
||||||
|
isDev := checkIsDevelopment()
|
||||||
|
runUpdateCheck(cfg.DatabasusHost, *isSkipUpdate, isDev, log)
|
||||||
|
|
||||||
|
if err := start.Run(cfg, log); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStop() {
|
||||||
|
logger.Init(false)
|
||||||
|
logger.GetLogger().Info("stop: stub — not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runStatus() {
|
||||||
|
logger.Init(false)
|
||||||
|
logger.GetLogger().Info("status: stub — not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRestore(args []string) {
|
||||||
|
fs := flag.NewFlagSet("restore", flag.ExitOnError)
|
||||||
|
|
||||||
|
targetDir := fs.String("target-dir", "", "Target pgdata directory")
|
||||||
|
backupID := fs.String("backup-id", "", "Full backup UUID (optional)")
|
||||||
|
targetTime := fs.String("target-time", "", "PITR target time in RFC3339 (optional)")
|
||||||
|
isYes := fs.Bool("yes", false, "Skip confirmation prompt")
|
||||||
|
isDebug := fs.Bool("debug", false, "Enable debug logging")
|
||||||
|
isSkipUpdate := fs.Bool("skip-update", false, "Skip auto-update check")
|
||||||
|
|
||||||
|
cfg := &config.Config{}
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, args)
|
||||||
|
|
||||||
|
if err := cfg.SaveToJSON(); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to save config: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Init(*isDebug)
|
||||||
|
log := logger.GetLogger()
|
||||||
|
|
||||||
|
isDev := checkIsDevelopment()
|
||||||
|
runUpdateCheck(cfg.DatabasusHost, *isSkipUpdate, isDev, log)
|
||||||
|
|
||||||
|
log.Info("restore: stub — not yet implemented",
|
||||||
|
"targetDir", *targetDir,
|
||||||
|
"backupId", *backupID,
|
||||||
|
"targetTime", *targetTime,
|
||||||
|
"yes", *isYes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Fprintln(os.Stderr, "Usage: databasus-agent <command> [flags]")
|
||||||
|
fmt.Fprintln(os.Stderr, "")
|
||||||
|
fmt.Fprintln(os.Stderr, "Commands:")
|
||||||
|
fmt.Fprintln(os.Stderr, " start Start the agent (WAL archiving + basebackups)")
|
||||||
|
fmt.Fprintln(os.Stderr, " stop Stop a running agent")
|
||||||
|
fmt.Fprintln(os.Stderr, " status Show agent status")
|
||||||
|
fmt.Fprintln(os.Stderr, " restore Restore a database from backup")
|
||||||
|
fmt.Fprintln(os.Stderr, " version Print agent version")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUpdateCheck(host string, isSkipUpdate bool, isDev bool, log interface {
|
||||||
|
Info(string, ...any)
|
||||||
|
Warn(string, ...any)
|
||||||
|
Error(string, ...any)
|
||||||
|
}) {
|
||||||
|
if isSkipUpdate {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if host == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := upgrade.CheckAndUpdate(host, Version, isDev, log); err != nil {
|
||||||
|
log.Error("Auto-update failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIsDevelopment() bool {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for range 3 {
|
||||||
|
if data, err := os.ReadFile(filepath.Join(dir, ".env")); err == nil {
|
||||||
|
return parseEnvMode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = filepath.Dir(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseEnvMode(data []byte) bool {
|
||||||
|
for line := range strings.SplitSeq(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) == 2 && strings.TrimSpace(parts[0]) == "ENV_MODE" {
|
||||||
|
return strings.TrimSpace(parts[1]) == "development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
11
agent/go.mod
Normal file
11
agent/go.mod
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module databasus-agent
|
||||||
|
|
||||||
|
go 1.24.9
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.11.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
10
agent/go.sum
Normal file
10
agent/go.sum
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
136
agent/internal/config/config.go
Normal file
136
agent/internal/config/config.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"databasus-agent/internal/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log = logger.GetLogger()
|
||||||
|
|
||||||
|
const configFileName = "databasus.json"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
DatabasusHost string `json:"databasusHost"`
|
||||||
|
DbID string `json:"dbId"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
|
||||||
|
flags parsedFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromJSONAndArgs reads databasus.json into the struct
|
||||||
|
// and overrides JSON values with any explicitly provided CLI flags.
|
||||||
|
func (c *Config) LoadFromJSONAndArgs(fs *flag.FlagSet, args []string) {
|
||||||
|
c.loadFromJSON()
|
||||||
|
c.initSources()
|
||||||
|
|
||||||
|
c.flags.host = fs.String(
|
||||||
|
"databasus-host",
|
||||||
|
"",
|
||||||
|
"Databasus server URL (e.g. http://your-server:4005)",
|
||||||
|
)
|
||||||
|
c.flags.dbID = fs.String("db-id", "", "Database ID")
|
||||||
|
c.flags.token = fs.String("token", "", "Agent token")
|
||||||
|
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.applyFlags()
|
||||||
|
log.Info("========= Loading config ============")
|
||||||
|
c.logConfigSources()
|
||||||
|
log.Info("========= Config has been loaded ====")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveToJSON writes the current struct to databasus.json.
|
||||||
|
func (c *Config) SaveToJSON() error {
|
||||||
|
data, err := json.MarshalIndent(c, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(configFileName, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) loadFromJSON() {
|
||||||
|
data, err := os.ReadFile(configFileName)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Info("No databasus.json found, will create on save")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warn("Failed to read databasus.json", "error", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, c); err != nil {
|
||||||
|
log.Warn("Failed to parse databasus.json", "error", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Configuration loaded from " + configFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) initSources() {
|
||||||
|
c.flags.sources = map[string]string{
|
||||||
|
"databasus-host": "not configured",
|
||||||
|
"db-id": "not configured",
|
||||||
|
"token": "not configured",
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DatabasusHost != "" {
|
||||||
|
c.flags.sources["databasus-host"] = configFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.DbID != "" {
|
||||||
|
c.flags.sources["db-id"] = configFileName
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Token != "" {
|
||||||
|
c.flags.sources["token"] = configFileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) applyFlags() {
|
||||||
|
if c.flags.host != nil && *c.flags.host != "" {
|
||||||
|
c.DatabasusHost = *c.flags.host
|
||||||
|
c.flags.sources["databasus-host"] = "command line args"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.flags.dbID != nil && *c.flags.dbID != "" {
|
||||||
|
c.DbID = *c.flags.dbID
|
||||||
|
c.flags.sources["db-id"] = "command line args"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.flags.token != nil && *c.flags.token != "" {
|
||||||
|
c.Token = *c.flags.token
|
||||||
|
c.flags.sources["token"] = "command line args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) logConfigSources() {
|
||||||
|
log.Info(
|
||||||
|
"databasus-host",
|
||||||
|
"value",
|
||||||
|
c.DatabasusHost,
|
||||||
|
"source",
|
||||||
|
c.flags.sources["databasus-host"],
|
||||||
|
)
|
||||||
|
log.Info("db-id", "value", c.DbID, "source", c.flags.sources["db-id"])
|
||||||
|
log.Info("token", "value", maskSensitive(c.Token), "source", c.flags.sources["token"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskSensitive(value string) string {
|
||||||
|
if value == "" {
|
||||||
|
return "(not set)"
|
||||||
|
}
|
||||||
|
|
||||||
|
visibleLen := max(len(value)/4, 1)
|
||||||
|
|
||||||
|
return value[:visibleLen] + "***"
|
||||||
|
}
|
||||||
162
agent/internal/config/config_test.go
Normal file
162
agent/internal/config/config_test.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_LoadFromJSONAndArgs_ValuesLoadedFromJSON(t *testing.T) {
|
||||||
|
dir := setupTempDir(t)
|
||||||
|
writeConfigJSON(t, dir, Config{
|
||||||
|
DatabasusHost: "http://json-host:4005",
|
||||||
|
DbID: "json-db-id",
|
||||||
|
Token: "json-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, []string{})
|
||||||
|
|
||||||
|
assert.Equal(t, "http://json-host:4005", cfg.DatabasusHost)
|
||||||
|
assert.Equal(t, "json-db-id", cfg.DbID)
|
||||||
|
assert.Equal(t, "json-token", cfg.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_LoadFromJSONAndArgs_ValuesLoadedFromArgs_WhenNoJSON(t *testing.T) {
|
||||||
|
setupTempDir(t)
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, []string{
|
||||||
|
"--databasus-host", "http://arg-host:4005",
|
||||||
|
"--db-id", "arg-db-id",
|
||||||
|
"--token", "arg-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "http://arg-host:4005", cfg.DatabasusHost)
|
||||||
|
assert.Equal(t, "arg-db-id", cfg.DbID)
|
||||||
|
assert.Equal(t, "arg-token", cfg.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_LoadFromJSONAndArgs_ArgsOverrideJSON(t *testing.T) {
|
||||||
|
dir := setupTempDir(t)
|
||||||
|
writeConfigJSON(t, dir, Config{
|
||||||
|
DatabasusHost: "http://json-host:4005",
|
||||||
|
DbID: "json-db-id",
|
||||||
|
Token: "json-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, []string{
|
||||||
|
"--databasus-host", "http://arg-host:9999",
|
||||||
|
"--db-id", "arg-db-id-override",
|
||||||
|
"--token", "arg-token-override",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "http://arg-host:9999", cfg.DatabasusHost)
|
||||||
|
assert.Equal(t, "arg-db-id-override", cfg.DbID)
|
||||||
|
assert.Equal(t, "arg-token-override", cfg.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_LoadFromJSONAndArgs_PartialArgsOverrideJSON(t *testing.T) {
|
||||||
|
dir := setupTempDir(t)
|
||||||
|
writeConfigJSON(t, dir, Config{
|
||||||
|
DatabasusHost: "http://json-host:4005",
|
||||||
|
DbID: "json-db-id",
|
||||||
|
Token: "json-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, []string{
|
||||||
|
"--databasus-host", "http://arg-host-only:4005",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "http://arg-host-only:4005", cfg.DatabasusHost)
|
||||||
|
assert.Equal(t, "json-db-id", cfg.DbID)
|
||||||
|
assert.Equal(t, "json-token", cfg.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SaveToJSON_ConfigSavedCorrectly(t *testing.T) {
|
||||||
|
setupTempDir(t)
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
DatabasusHost: "http://save-host:4005",
|
||||||
|
DbID: "save-db-id",
|
||||||
|
Token: "save-token",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := cfg.SaveToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved := readConfigJSON(t)
|
||||||
|
|
||||||
|
assert.Equal(t, "http://save-host:4005", saved.DatabasusHost)
|
||||||
|
assert.Equal(t, "save-db-id", saved.DbID)
|
||||||
|
assert.Equal(t, "save-token", saved.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SaveToJSON_AfterArgsOverrideJSON_SavedFileContainsMergedValues(t *testing.T) {
|
||||||
|
dir := setupTempDir(t)
|
||||||
|
writeConfigJSON(t, dir, Config{
|
||||||
|
DatabasusHost: "http://json-host:4005",
|
||||||
|
DbID: "json-db-id",
|
||||||
|
Token: "json-token",
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := &Config{}
|
||||||
|
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||||
|
cfg.LoadFromJSONAndArgs(fs, []string{
|
||||||
|
"--databasus-host", "http://override-host:9999",
|
||||||
|
})
|
||||||
|
|
||||||
|
err := cfg.SaveToJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
saved := readConfigJSON(t)
|
||||||
|
|
||||||
|
assert.Equal(t, "http://override-host:9999", saved.DatabasusHost)
|
||||||
|
assert.Equal(t, "json-db-id", saved.DbID)
|
||||||
|
assert.Equal(t, "json-token", saved.Token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTempDir(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
origDir, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
require.NoError(t, os.Chdir(dir))
|
||||||
|
|
||||||
|
t.Cleanup(func() { os.Chdir(origDir) })
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfigJSON(t *testing.T, dir string, cfg Config) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(dir+"/"+configFileName, data, 0644))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigJSON(t *testing.T) Config {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configFileName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
require.NoError(t, json.Unmarshal(data, &cfg))
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
9
agent/internal/config/dto.go
Normal file
9
agent/internal/config/dto.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type parsedFlags struct {
|
||||||
|
host *string
|
||||||
|
dbID *string
|
||||||
|
token *string
|
||||||
|
|
||||||
|
sources map[string]string
|
||||||
|
}
|
||||||
37
agent/internal/features/start/start.go
Normal file
37
agent/internal/features/start/start.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package start
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"databasus-agent/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Run(cfg *config.Config, log *slog.Logger) error {
|
||||||
|
if err := validateConfig(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("start: stub — not yet implemented",
|
||||||
|
"dbId", cfg.DbID,
|
||||||
|
"hasToken", cfg.Token != "",
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConfig(cfg *config.Config) error {
|
||||||
|
if cfg.DatabasusHost == "" {
|
||||||
|
return errors.New("argument databasus-host is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.DbID == "" {
|
||||||
|
return errors.New("argument db-id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Token == "" {
|
||||||
|
return errors.New("argument token is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
142
agent/internal/features/upgrade/upgrader.go
Normal file
142
agent/internal/features/upgrade/upgrader.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package upgrade
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Info(msg string, args ...any)
|
||||||
|
Warn(msg string, args ...any)
|
||||||
|
Error(msg string, args ...any)
|
||||||
|
}
|
||||||
|
|
||||||
|
type versionResponse struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckAndUpdate(databasusHost, currentVersion string, isDev bool, log Logger) error {
|
||||||
|
if isDev {
|
||||||
|
log.Info("Skipping update check (development mode)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
serverVersion, err := fetchServerVersion(databasusHost, log)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverVersion == currentVersion {
|
||||||
|
log.Info("Agent version is up to date", "version", currentVersion)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Updating agent...", "current", currentVersion, "target", serverVersion)
|
||||||
|
|
||||||
|
selfPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to determine executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempPath := selfPath + ".update"
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = os.Remove(tempPath)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := downloadBinary(databasusHost, tempPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to download update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chmod(tempPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to set permissions on update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := verifyBinary(tempPath, serverVersion); err != nil {
|
||||||
|
return fmt.Errorf("update verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tempPath, selfPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to replace binary (try --skip-update if this persists): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Update complete, re-executing...")
|
||||||
|
|
||||||
|
return syscall.Exec(selfPath, os.Args, os.Environ())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchServerVersion(host string, log Logger) (string, error) {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
resp, err := client.Get(host + "/api/v1/system/version")
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("Could not reach server for update check, continuing", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Warn(
|
||||||
|
"Server returned non-OK status for version check, continuing",
|
||||||
|
"status",
|
||||||
|
resp.StatusCode,
|
||||||
|
)
|
||||||
|
return "", fmt.Errorf("status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ver versionResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&ver); err != nil {
|
||||||
|
log.Warn("Failed to parse server version response, continuing", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ver.Version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadBinary(host, destPath string) error {
|
||||||
|
url := fmt.Sprintf("%s/api/v1/system/agent?arch=%s", host, runtime.GOARCH)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("server returned %d for agent download", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = f.Close() }()
|
||||||
|
|
||||||
|
_, err = io.Copy(f, resp.Body)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyBinary(binaryPath, expectedVersion string) error {
|
||||||
|
cmd := exec.Command(binaryPath, "version")
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("binary failed to execute: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := strings.TrimSpace(string(output))
|
||||||
|
if got != expectedVersion {
|
||||||
|
return fmt.Errorf("version mismatch: expected %q, got %q", expectedVersion, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
47
agent/internal/logger/logger.go
Normal file
47
agent/internal/logger/logger.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loggerInstance *slog.Logger
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(isDebug bool) {
|
||||||
|
level := slog.LevelInfo
|
||||||
|
if isDebug {
|
||||||
|
level = slog.LevelDebug
|
||||||
|
}
|
||||||
|
|
||||||
|
once.Do(func() {
|
||||||
|
loggerInstance = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: level,
|
||||||
|
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||||
|
if a.Key == slog.TimeKey {
|
||||||
|
a.Value = slog.StringValue(time.Now().Format("2006/01/02 15:04:05"))
|
||||||
|
}
|
||||||
|
if a.Key == slog.LevelKey {
|
||||||
|
return slog.Attr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
loggerInstance.Info("Text structured logger initialized")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogger returns a singleton slog.Logger that logs to the console
|
||||||
|
func GetLogger() *slog.Logger {
|
||||||
|
if loggerInstance == nil {
|
||||||
|
Init(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return loggerInstance
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"databasus-backend/internal/features/restores"
|
"databasus-backend/internal/features/restores"
|
||||||
"databasus-backend/internal/features/restores/restoring"
|
"databasus-backend/internal/features/restores/restoring"
|
||||||
"databasus-backend/internal/features/storages"
|
"databasus-backend/internal/features/storages"
|
||||||
|
system_agent "databasus-backend/internal/features/system/agent"
|
||||||
system_healthcheck "databasus-backend/internal/features/system/healthcheck"
|
system_healthcheck "databasus-backend/internal/features/system/healthcheck"
|
||||||
system_version "databasus-backend/internal/features/system/version"
|
system_version "databasus-backend/internal/features/system/version"
|
||||||
task_cancellation "databasus-backend/internal/features/tasks/cancellation"
|
task_cancellation "databasus-backend/internal/features/tasks/cancellation"
|
||||||
@@ -212,6 +213,7 @@ func setUpRoutes(r *gin.Engine) {
|
|||||||
userController.RegisterRoutes(v1)
|
userController.RegisterRoutes(v1)
|
||||||
system_healthcheck.GetHealthcheckController().RegisterRoutes(v1)
|
system_healthcheck.GetHealthcheckController().RegisterRoutes(v1)
|
||||||
system_version.GetVersionController().RegisterRoutes(v1)
|
system_version.GetVersionController().RegisterRoutes(v1)
|
||||||
|
system_agent.GetAgentController().RegisterRoutes(v1)
|
||||||
backups_controllers.GetBackupController().RegisterPublicRoutes(v1)
|
backups_controllers.GetBackupController().RegisterPublicRoutes(v1)
|
||||||
backups_controllers.GetPostgresWalBackupController().RegisterRoutes(v1)
|
backups_controllers.GetPostgresWalBackupController().RegisterRoutes(v1)
|
||||||
databases.GetDatabaseController().RegisterPublicRoutes(v1)
|
databases.GetDatabaseController().RegisterPublicRoutes(v1)
|
||||||
|
|||||||
48
backend/internal/features/system/agent/controller.go
Normal file
48
backend/internal/features/system/agent/controller.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package system_agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AgentController struct{}
|
||||||
|
|
||||||
|
func (c *AgentController) RegisterRoutes(router *gin.RouterGroup) {
|
||||||
|
router.GET("/system/agent", c.DownloadAgent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DownloadAgent
|
||||||
|
// @Summary Download agent binary
|
||||||
|
// @Description Download the databasus-agent binary for the specified architecture
|
||||||
|
// @Tags system/agent
|
||||||
|
// @Produce octet-stream
|
||||||
|
// @Param arch query string true "Target architecture" Enums(amd64, arm64)
|
||||||
|
// @Success 200 {file} binary
|
||||||
|
// @Failure 400 {object} map[string]string
|
||||||
|
// @Failure 404 {object} map[string]string
|
||||||
|
// @Router /system/agent [get]
|
||||||
|
func (c *AgentController) DownloadAgent(ctx *gin.Context) {
|
||||||
|
arch := ctx.Query("arch")
|
||||||
|
if arch != "amd64" && arch != "arm64" {
|
||||||
|
ctx.JSON(http.StatusBadRequest, gin.H{"error": "arch must be amd64 or arm64"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binaryName := "databasus-agent-linux-" + arch
|
||||||
|
binaryPath := filepath.Join("agent-binaries", binaryName)
|
||||||
|
|
||||||
|
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
|
||||||
|
ctx.JSON(
|
||||||
|
http.StatusNotFound,
|
||||||
|
gin.H{"error": "agent binary not found for architecture: " + arch},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Header("Content-Type", "application/octet-stream")
|
||||||
|
ctx.Header("Content-Disposition", "attachment; filename=databasus-agent")
|
||||||
|
ctx.File(binaryPath)
|
||||||
|
}
|
||||||
7
backend/internal/features/system/agent/di.go
Normal file
7
backend/internal/features/system/agent/di.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package system_agent
|
||||||
|
|
||||||
|
var agentController = &AgentController{}
|
||||||
|
|
||||||
|
func GetAgentController() *AgentController {
|
||||||
|
return agentController
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user