docker-volume-backup/src/main.go

202 lines
4.8 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"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"
)
func main() {
s := &script{}
defer func() {
for _, thunk := range s.cleanupTasks {
thunk()
}
}()
s.lock()
defer s.unlock()
if err := s.init(); err != nil {
panic(err)
}
if err := s.stopContainers(); err != nil {
panic(err)
}
if err := s.takeBackup(); err != nil {
panic(err)
}
if err := s.restartContainers(); err != nil {
panic(err)
}
if err := s.encryptBackup(); err != nil {
panic(err)
}
if err := s.copyBackup(); err != nil {
panic(err)
}
if err := s.cleanBackup(); err != nil {
panic(err)
}
if err := s.pruneBackups(); err != nil {
panic(err)
}
}
type script struct {
ctx context.Context
cli *client.Client
stoppedContainers []types.Container
file string
cleanupTasks []func()
}
func (s *script) lock() {}
func (s *script) unlock() {}
func (s *script) init() error {
s.ctx = context.Background()
if timeout, ok := os.LookupEnv("BACKUP_TIMEOUT_DURATION"); ok {
d, err := time.ParseDuration(timeout)
if err != nil {
return fmt.Errorf("init: error parsing given timeout duration: %w", err)
}
withTimeout, cancelFunc := context.WithTimeout(context.Background(), d)
s.ctx = withTimeout
s.cleanupTasks = append(s.cleanupTasks, cancelFunc)
}
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 {
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return fmt.Errorf("init: failied to create docker client")
}
s.cli = cli
}
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")),
}),
})
s.stoppedContainers = stoppedContainers
if err != nil {
return fmt.Errorf("stopContainers: error querying for containers to stop: %w", err)
}
fmt.Printf("Stopping %d containers\n", len(s.stoppedContainers))
if len(s.stoppedContainers) != 0 {
fmt.Println("Stopping containers")
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)
}
}
}
return nil
}
func (s *script) takeBackup() error {
return errors.New("takeBackup: not implemented yet")
}
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
}
if err := s.cli.ContainerStart(s.ctx, container.ID, types.ContainerStartOptions{}); err != nil {
panic(err)
}
}
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
}
}
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{},
)
}
}
return nil
}
func (s *script) encryptBackup() error {
_, ok := os.LookupEnv("GPG_PASSPHRASE")
if !ok {
return nil
}
return errors.New("encryptBackup: not implemented yet")
}
func (s *script) copyBackup() error {
return errors.New("copyBackup: not implemented yet")
}
func (s *script) cleanBackup() error {
return errors.New("cleanBackup: not implemented yet")
}
func (s *script) pruneBackups() error {
_, ok := os.LookupEnv("BACKUP_RETENTION_DAYS")
if !ok {
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
}