feat: now fully functionnal api
This commit is contained in:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,8 +1,9 @@
|
||||
backups
|
||||
./config.yaml
|
||||
compose.yaml
|
||||
./ddbb
|
||||
./*.sql
|
||||
./*.bson
|
||||
./*.archive
|
||||
./*.gz
|
||||
ddbb
|
||||
main
|
||||
*.sql
|
||||
*.bson
|
||||
*.archive
|
||||
*.gz
|
||||
|
||||
@@ -9,8 +9,7 @@ RUN go mod download
|
||||
|
||||
# Copy all source code
|
||||
COPY cmd/ /app/cmd/
|
||||
COPY pkg/ /app/pkg/
|
||||
COPY templates/ /app/templates/
|
||||
COPY internal/ /app/internal/
|
||||
|
||||
# Build the binary
|
||||
RUN go build -o ddbb ./cmd/ddbb/main.go
|
||||
@@ -20,12 +19,11 @@ FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/ddbb .
|
||||
COPY --from=builder /app/templates/ templates/
|
||||
|
||||
RUN apk update
|
||||
RUN apk add bash mariadb-client postgresql17-client mongodb-tools
|
||||
|
||||
EXPOSE 12345
|
||||
EXPOSE 3000
|
||||
|
||||
# Run the server
|
||||
CMD ["./ddbb"]
|
||||
|
||||
@@ -2,18 +2,21 @@ package main
|
||||
|
||||
import (
|
||||
"ddbb/internal/api"
|
||||
// "ddbb/pkg/cron"
|
||||
"ddbb/internal/cron"
|
||||
"log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set up the cron job
|
||||
// cron.ScheduleBackupFromEnv()
|
||||
cronErr := cron.SetupNewCronJob()
|
||||
if cronErr != nil {
|
||||
log.Fatalf("Failed scheduling cron task: %v", cronErr)
|
||||
}
|
||||
|
||||
// Run the api endpoint
|
||||
r := api.SetupRouter()
|
||||
err := r.Run(":3000")
|
||||
if err != nil {
|
||||
log.Fatalf("API fatal error: %v", err)
|
||||
apiErr := r.Run(":3000")
|
||||
if apiErr != nil {
|
||||
log.Fatalf("API fatal error: %v", apiErr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +1,112 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
"ddbb/internal/docker"
|
||||
"ddbb/internal/dump"
|
||||
"ddbb/internal/integrity"
|
||||
"ddbb/internal/restore"
|
||||
"ddbb/internal/types"
|
||||
"log"
|
||||
"net/http"
|
||||
"slices"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"-"` // omit from JSON responses
|
||||
DumpFile string `json:"dump_file"`
|
||||
DumpFileModTime time.Time `json:"dump_file_mod_time"`
|
||||
IntegrityStatus string `json:"integrity_status"`
|
||||
IntegrityLastCheckTime time.Time `json:"integrity_last_check_time"`
|
||||
func selectJob(id string) types.Job {
|
||||
var chosenJob *types.Job
|
||||
jobs, err := docker.GetConfigsFromContainers()
|
||||
if err != nil {
|
||||
log.Fatalf("Error while parsing docker labels: %v", err)
|
||||
}
|
||||
for i, job := range jobs {
|
||||
if job.Id == id {
|
||||
chosenJob = &jobs[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
return *chosenJob
|
||||
}
|
||||
|
||||
type DumpJobResult struct {
|
||||
Id string
|
||||
TimeStart time.Time
|
||||
TimeEnd time.Time
|
||||
Result string
|
||||
Error error
|
||||
func isValidJobId(id string) bool {
|
||||
var idLists []string
|
||||
jobs, err := docker.GetConfigsFromContainers()
|
||||
if err != nil {
|
||||
log.Fatalf("Error while parsing docker labels: %v", err)
|
||||
}
|
||||
for _, job := range jobs {
|
||||
idLists = append(idLists, job.Id)
|
||||
}
|
||||
|
||||
return slices.Contains(idLists, id)
|
||||
}
|
||||
|
||||
type RestoreJobResult struct {
|
||||
Id string
|
||||
TimeStart time.Time
|
||||
TimeEnd time.Time
|
||||
Result string
|
||||
Error error
|
||||
}
|
||||
|
||||
func listJobs() error {
|
||||
return nil
|
||||
func listJobs(c *gin.Context) {
|
||||
jobs, err := docker.GetConfigsFromContainers()
|
||||
if err != nil {
|
||||
log.Fatalf("Error while parsing docker labels: %v", err)
|
||||
}
|
||||
c.JSON(200, jobs)
|
||||
}
|
||||
|
||||
// Dump any database based on Id
|
||||
func triggerDump(c *gin.Context) DumpJobResult {
|
||||
return DumpJobResult{}
|
||||
func triggerDump(c *gin.Context) {
|
||||
jobId := c.PostForm("id")
|
||||
if jobId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing backup job id"})
|
||||
return
|
||||
}
|
||||
if isValidJobId(jobId) {
|
||||
result, err := dump.NewDumpJob(selectJob(jobId))
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
c.JSON(200, result)
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Invalid job id"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Restore any database based on Id
|
||||
func triggerRestore(c *gin.Context) RestoreJobResult {
|
||||
return RestoreJobResult{}
|
||||
func triggerRestore(c *gin.Context) {
|
||||
jobId := c.PostForm("id")
|
||||
if jobId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing backup job id"})
|
||||
return
|
||||
}
|
||||
if isValidJobId(jobId) {
|
||||
result, err := restore.NewRestoreJob(selectJob(jobId))
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
c.JSON(200, result)
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Invalid job id"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check any database based on Id
|
||||
func triggerIntegrityCheck(c *gin.Context) Job {
|
||||
return Job{}
|
||||
func triggerIntegrityCheck(c *gin.Context) {
|
||||
jobId := c.PostForm("id")
|
||||
if jobId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing backup job id"})
|
||||
return
|
||||
}
|
||||
if isValidJobId(jobId) {
|
||||
result, err := integrity.NewIntegrityCheckJob(selectJob(jobId))
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
c.JSON(200, result)
|
||||
return
|
||||
} else {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Invalid job id"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// API entrypoint
|
||||
@@ -60,7 +114,10 @@ func SetupRouter() *gin.Engine {
|
||||
// GIN General setup
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
r.SetTrustedProxies([]string{"127.0.0.1"})
|
||||
err := r.SetTrustedProxies([]string{"127.0.0.1"})
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
|
||||
// GIN endpoints
|
||||
r.GET("/list", listJobs)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package backup
|
||||
@@ -0,0 +1,5 @@
|
||||
package cron
|
||||
|
||||
func SetupNewCronJob() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"ddbb/internal/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetConfigsFromContainers() ([]types.Job, error) {
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containers, err := cli.ContainerList(context.Background(), container.ListOptions{All: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jobs []types.Job
|
||||
|
||||
for _, container := range containers {
|
||||
if container.Labels["ddbb.enable"] != "true" {
|
||||
continue
|
||||
}
|
||||
|
||||
kind := container.Labels["ddbb.kind"]
|
||||
id := container.ID[:12]
|
||||
|
||||
inspect, err := cli.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
envMap := map[string]string{}
|
||||
for _, env := range inspect.Config.Env {
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
envMap[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
var host, port, database, user, password string
|
||||
|
||||
// Set hostname
|
||||
if len(container.Names) > 0 {
|
||||
host = strings.TrimPrefix(container.Names[0], "/")
|
||||
} else {
|
||||
// fallback
|
||||
host = id
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case "mariadb":
|
||||
port = "3306"
|
||||
database = envMap["MARIADB_DATABASE"]
|
||||
user = envMap["MARIADB_USER"]
|
||||
password = envMap["MARIADB_PASSWORD"]
|
||||
case "postgres":
|
||||
port = "5432"
|
||||
database = envMap["POSTGRES_DB"]
|
||||
user = envMap["POSTGRES_USER"]
|
||||
password = envMap["POSTGRES_PASSWORD"]
|
||||
case "mongodb":
|
||||
port = "27017"
|
||||
database = envMap["MONGO_INITDB_DATABASE"]
|
||||
user = envMap["MONGO_INITDB_ROOT_USERNAME"]
|
||||
password = envMap["MONGO_INITDB_ROOT_PASSWORD"]
|
||||
default:
|
||||
log.Printf("Kind %s is not supported", kind)
|
||||
}
|
||||
|
||||
jobs = append(jobs, types.Job{
|
||||
Id: id,
|
||||
Kind: kind,
|
||||
Host: host,
|
||||
Port: port,
|
||||
Database: database,
|
||||
User: user,
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
113
internal/dump/dump.go
Normal file
113
internal/dump/dump.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package dump
|
||||
|
||||
import (
|
||||
"ddbb/internal/types"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var outFileRoot = "/backups/"
|
||||
|
||||
func NewDumpJob(job types.Job) (types.DumpJobResult, error) {
|
||||
var err error
|
||||
switch strings.ToLower(job.Kind) {
|
||||
case "mariadb":
|
||||
err = dumpMariaDB(job)
|
||||
case "postgres":
|
||||
err = dumpPostgres(job)
|
||||
case "mongodb":
|
||||
err = dumpMongoDB(job)
|
||||
default:
|
||||
log.Printf("Invalid database kind for job %s", job.Id)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
return types.DumpJobResult{}, nil
|
||||
}
|
||||
|
||||
func dumpMariaDB(job types.Job) error {
|
||||
log.Printf("Starting dump of mariadb database %s on %s", job.Database, job.Host)
|
||||
|
||||
outFileName := outFileRoot + job.Host + ".sql"
|
||||
outFile, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create output file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
cmd := exec.Command(
|
||||
"mariadb-dump",
|
||||
"--ssl-verify-server-cert=FALSE",
|
||||
"-h", job.Host,
|
||||
"-u", job.User,
|
||||
"-p"+job.Password,
|
||||
"-P", job.Port,
|
||||
job.Database,
|
||||
)
|
||||
cmd.Stdout = outFile
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("Mariadb dump failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Database backup saved to %s", outFileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpPostgres(job types.Job) error {
|
||||
outFileName := outFileRoot + job.Host + ".sql"
|
||||
outFile, err := os.Create(outFileName)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create output file: %v", err)
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Build pg_dump command
|
||||
cmd := exec.Command(
|
||||
"pg_dump",
|
||||
"--dbname="+fmt.Sprintf("postgresql://%s:%s@%s:%s/%s", job.User, job.Password, job.Host, job.Port, job.Database),
|
||||
)
|
||||
cmd.Stdout = outFile
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
log.Printf("Running pg_dump for database %s", job.Database)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("pg_dump failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Database backup saved to %s", outFileName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func dumpMongoDB(job types.Job) error {
|
||||
outFileName := outFileRoot + job.Host + ".gz"
|
||||
|
||||
cmd := exec.Command(
|
||||
"mongodump",
|
||||
"--uri",
|
||||
fmt.Sprintf("mongodb://%s:%s@%s:%s/%s?authSource=admin", job.User, job.Password, job.Host, job.Port, job.Database),
|
||||
"--archive="+outFileName,
|
||||
"--gzip",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
log.Printf("Running mongodump for database %s with gzip compression to file %s", job.Database, outFileName)
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("mongodump failed: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("MongoDB backup saved to compressed archive %s", outFileName)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package integrity
|
||||
|
||||
import (
|
||||
"ddbb/internal/types"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func NewIntegrityCheckJob(job types.Job) (types.DumpJobResult, error) {
|
||||
var err error
|
||||
switch strings.ToLower(job.Kind) {
|
||||
case "mariadb":
|
||||
err = checkMariaDB(job)
|
||||
case "postgres":
|
||||
err = checkPostgres(job)
|
||||
case "mongodb":
|
||||
err = checkMongoDB(job)
|
||||
default:
|
||||
log.Printf("Invalid database kind for job %s", job.Id)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
}
|
||||
return types.DumpJobResult{}, nil
|
||||
}
|
||||
|
||||
func checkMariaDB(job types.Job) error {
|
||||
log.Println("Not yet implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkPostgres(job types.Job) error {
|
||||
log.Println("Not yet implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkMongoDB(job types.Job) error {
|
||||
log.Println("Not yet implemented")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package restore
|
||||
|
||||
import (
|
||||
"ddbb/internal/types"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var dumpFileRoot = "/backups/"
|
||||
|
||||
func NewRestoreJob(job types.Job) (types.RestoreJobResult, error) {
|
||||
switch strings.ToLower(job.Kind) {
|
||||
case "mariadb":
|
||||
restoreMariaDB(job)
|
||||
case "postgres":
|
||||
restorePostgres(job)
|
||||
case "mongodb":
|
||||
restoreMongo(job)
|
||||
default:
|
||||
log.Printf("unsupported database kind: %s", job.Kind)
|
||||
}
|
||||
return types.RestoreJobResult{}, nil
|
||||
}
|
||||
|
||||
func restoreMariaDB(job types.Job) error {
|
||||
cmd := exec.Command(
|
||||
"mariadb",
|
||||
"--ssl-verify-server-cert=FALSE",
|
||||
"-h", job.Host,
|
||||
"-u", job.User,
|
||||
fmt.Sprintf("-p%s", job.Password),
|
||||
job.Database,
|
||||
)
|
||||
dumpFileName := dumpFileRoot + job.Host + ".sql"
|
||||
dumpFile, err := os.Open(dumpFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dumpFile.Close()
|
||||
cmd.Stdin = dumpFile
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("mysql restore failed: %s, output: %s", err, output)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Restore job with ID %s completed successfully", job.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func restorePostgres(job types.Job) error {
|
||||
cmd := exec.Command(
|
||||
"psql", "-h", job.Host, "-U", job.User, "-d", job.Database,
|
||||
"-f", "-", // read input from stdin
|
||||
)
|
||||
dumpFileName := dumpFileRoot + job.Host + ".sql"
|
||||
dumpFile, err := os.Open(dumpFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dumpFile.Close()
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("PGPASSWORD=%s", job.Password))
|
||||
cmd.Stdin = dumpFile
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("postgres restore failed: %s, output: %s", err, output)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Restore job with ID %s completed successfully", job.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreMongo(job types.Job) error {
|
||||
dumpFileName := dumpFileRoot + job.Host + ".gz"
|
||||
cmd := exec.Command(
|
||||
"mongorestore", "--archive", "--gzip", "--drop", "--archive="+dumpFileName,
|
||||
"--uri",
|
||||
fmt.Sprintf("mongodb://%s:%s@%s:%s/%s?authSource=admin", job.User, job.Password, job.Host, job.Port, job.Database),
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("mongodb restore failed: %s, output: %s", err, output)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Restore job with ID %s completed successfully", job.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
33
internal/types/types.go
Normal file
33
internal/types/types.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package types
|
||||
|
||||
import "time"
|
||||
|
||||
type Job struct {
|
||||
Id string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Host string `json:"host"`
|
||||
Port string `json:"port"`
|
||||
Database string `json:"database"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"-"` // omit from JSON responses
|
||||
DumpFile string `json:"dump_file"`
|
||||
DumpFileModTime time.Time `json:"dump_file_mod_time"`
|
||||
IntegrityStatus string `json:"integrity_status"`
|
||||
IntegrityLastCheckTime time.Time `json:"integrity_last_check_time"`
|
||||
}
|
||||
|
||||
type DumpJobResult struct {
|
||||
Id string
|
||||
TimeStart time.Time
|
||||
TimeEnd time.Time
|
||||
Result string
|
||||
Error error
|
||||
}
|
||||
|
||||
type RestoreJobResult struct {
|
||||
Id string
|
||||
TimeStart time.Time
|
||||
TimeEnd time.Time
|
||||
Result string
|
||||
Error error
|
||||
}
|
||||
Reference in New Issue
Block a user