docker-volume-backup/src/main.go

240 lines
5.8 KiB
Go
Raw Normal View History

package main
import (
"context"
2021-08-21 19:26:42 +02:00
"errors"
"fmt"
2021-08-21 21:26:27 +02:00
"io"
"os"
2021-08-21 21:26:27 +02:00
"path"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/client"
"github.com/joho/godotenv"
2021-08-21 21:26:27 +02:00
minio "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/walle/targz"
)
func main() {
2021-08-21 19:26:42 +02:00
s := &script{}
s.lock()
defer s.unlock()
2021-08-21 21:26:27 +02:00
must(s.init)()
must(s.stopContainers)()
must(s.takeBackup)()
must(s.restartContainers)()
must(s.encryptBackup)()
must(s.copyBackup)()
must(s.cleanBackup)()
must(s.pruneBackups)()
2021-08-21 19:26:42 +02:00
}
type script struct {
ctx context.Context
cli *client.Client
2021-08-21 21:26:27 +02:00
mc *minio.Client
2021-08-21 19:26:42 +02:00
stoppedContainers []types.Container
file string
}
func (s *script) lock() {}
func (s *script) unlock() {}
func (s *script) init() error {
s.ctx = context.Background()
if err := godotenv.Load("/etc/backup.env"); err != nil {
return fmt.Errorf("init: failed to load env file: %w", err)
}
socketExists, err := fileExists("/var/run/docker.sock")
if err != nil {
return fmt.Errorf("init: error checking whether docker.sock is available: %w", err)
}
if socketExists {
2021-08-21 19:26:42 +02:00
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
2021-08-21 19:26:42 +02:00
return fmt.Errorf("init: failied to create docker client")
}
2021-08-21 19:26:42 +02:00
s.cli = cli
}
2021-08-21 21:26:27 +02:00
if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" {
mc, err := minio.New(os.Getenv("AWS_ENDPOINT"), &minio.Options{
Creds: credentials.NewStaticV4(
os.Getenv("AWS_ACCESS_KEY_ID"),
os.Getenv("AWS_SECRET_ACCESS_KEY"),
"",
),
Secure: os.Getenv("AWS_ENDPOINT_PROTO") == "https",
})
if err != nil {
return fmt.Errorf("init: error setting up minio client: %w", err)
}
s.mc = mc
}
2021-08-21 19:26:42 +02:00
return nil
}
func (s *script) stopContainers() error {
if s.cli == nil {
return nil
}
stoppedContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{
Quiet: true,
Filters: filters.NewArgs(filters.KeyValuePair{
Key: "label",
Value: fmt.Sprintf("docker-volume-backup.stop-during-backup=%s", os.Getenv("BACKUP_STOP_CONTAINER_LABEL")),
}),
})
if err != nil {
return fmt.Errorf("stopContainers: error querying for containers to stop: %w", err)
}
2021-08-21 21:26:27 +02:00
fmt.Printf("Stopping %d containers\n", len(stoppedContainers))
2021-08-21 21:26:27 +02:00
if len(stoppedContainers) != 0 {
fmt.Println("Stopping containers")
2021-08-21 19:26:42 +02:00
for _, container := range s.stoppedContainers {
if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil {
return fmt.Errorf("stopContainers: error stopping container %s: %w", container.Names[0], err)
}
}
}
2021-08-21 21:26:27 +02:00
s.stoppedContainers = stoppedContainers
2021-08-21 19:26:42 +02:00
return nil
}
2021-08-21 19:26:42 +02:00
func (s *script) takeBackup() error {
2021-08-21 21:26:27 +02:00
file := os.Getenv("BACKUP_FILENAME")
if file == "" {
return errors.New("takeBackup: BACKUP_FILENAME not given")
}
s.file = file
if err := targz.Compress(os.Getenv("BACKUP_SOURCES"), s.file); err != nil {
return fmt.Errorf("takeBackup: error compressing backup folder: %w", err)
}
return nil
2021-08-21 19:26:42 +02:00
}
2021-08-21 19:26:42 +02:00
func (s *script) restartContainers() error {
fmt.Println("Starting containers/services back up")
servicesRequiringUpdate := map[string]struct{}{}
for _, container := range s.stoppedContainers {
if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok {
servicesRequiringUpdate[swarmServiceName] = struct{}{}
continue
}
2021-08-21 19:26:42 +02:00
if err := s.cli.ContainerStart(s.ctx, container.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}
}
2021-08-21 19:26:42 +02:00
if len(servicesRequiringUpdate) != 0 {
services, _ := s.cli.ServiceList(s.ctx, types.ServiceListOptions{})
for serviceName := range servicesRequiringUpdate {
var serviceMatch swarm.Service
for _, service := range services {
if service.Spec.Name == serviceName {
serviceMatch = service
break
}
}
2021-08-21 19:26:42 +02:00
if serviceMatch.ID == "" {
return fmt.Errorf("restartContainers: Couldn't find service with name %s", serviceName)
}
serviceMatch.Spec.TaskTemplate.ForceUpdate = 1
s.cli.ServiceUpdate(
s.ctx, serviceMatch.ID,
serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{},
)
}
}
2021-08-21 19:26:42 +02:00
return nil
}
func (s *script) encryptBackup() error {
2021-08-21 21:26:27 +02:00
key := os.Getenv("GPG_PASSPHRASE")
if key == "" {
2021-08-21 19:26:42 +02:00
return nil
}
return errors.New("encryptBackup: not implemented yet")
}
func (s *script) copyBackup() error {
2021-08-21 21:26:27 +02:00
_, name := path.Split(s.file)
if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" {
_, err := s.mc.FPutObject(s.ctx, bucket, name, s.file, minio.PutObjectOptions{
ContentType: "application/tar+gzip",
})
if err != nil {
return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err)
}
}
if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" {
if _, err := os.Stat(archive); !os.IsNotExist(err) {
if err := copy(s.file, path.Join(archive, name)); err != nil {
return fmt.Errorf("copyBackup: error copying file to local archive: %w", err)
}
}
}
return nil
2021-08-21 19:26:42 +02:00
}
func (s *script) cleanBackup() error {
2021-08-21 21:26:27 +02:00
if err := os.Remove(s.file); err != nil {
return fmt.Errorf("cleanBackup: error removing file: %w", err)
}
return nil
2021-08-21 19:26:42 +02:00
}
func (s *script) pruneBackups() error {
2021-08-21 21:26:27 +02:00
retention := os.Getenv("BACKUP_RETENTION_DAYS")
if retention == "" {
2021-08-21 19:26:42 +02:00
return nil
}
return errors.New("pruneBackups: not implemented yet")
}
func fileExists(location string) (bool, error) {
_, err := os.Stat(location)
if err != nil && err != os.ErrNotExist {
return false, err
}
return err == nil, nil
}
2021-08-21 21:26:27 +02:00
func must(f func() error) func() {
return func() {
if err := f(); err != nil {
panic(err)
}
}
}
func copy(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Close()
}