package main import ( "context" "errors" "fmt" "io" "os" "os/exec" "path" "strings" "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" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/walle/targz" ) func main() { s := &script{} must(s.lock)() defer s.unlock() must(s.init)() fmt.Println("Successfully initialized resources.") must(s.stopContainers)() fmt.Println("Successfully stopped containers.") must(s.takeBackup)() fmt.Println("Successfully took backup.") must(s.restartContainers)() fmt.Println("Successfully restarted containers.") must(s.encryptBackup)() fmt.Println("Successfully encrypted backup.") must(s.copyBackup)() fmt.Println("Successfully copied backup.") must(s.cleanBackup)() fmt.Println("Successfully cleaned local backup.") must(s.pruneOldBackups)() fmt.Println("Successfully pruned old backup.") } type script struct { ctx context.Context cli *client.Client mc *minio.Client stoppedContainers []types.Container releaseLock func() error file string } func (s *script) lock() error { lf, err := os.OpenFile("/var/dockervolumebackup.lock", os.O_CREATE, os.ModeAppend) if err != nil { return fmt.Errorf("lock: error opening lock file: %w", err) } s.releaseLock = lf.Close return nil } func (s *script) unlock() error { if err := s.releaseLock(); err != nil { return fmt.Errorf("unlock: error releasing file lock: %w", err) } if err := os.Remove("/var/dockervolumebackup.lock"); err != nil { return fmt.Errorf("unlock: error removing lock file: %w", err) } return nil } 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 { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return fmt.Errorf("init: failied to create docker client") } s.cli = cli } 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 } return nil } func (s *script) stopContainers() error { if s.cli == nil { return nil } allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, }) if err != nil { return fmt.Errorf("stopContainers: error querying for containers: %w", err) } containersToStop, 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) } fmt.Printf("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) if len(containersToStop) != 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) } } } s.stoppedContainers = containersToStop return nil } func (s *script) takeBackup() error { if os.Getenv("BACKUP_FILENAME") == "" { return errors.New("takeBackup: BACKUP_FILENAME not given") } outBytes, err := exec.Command("date", fmt.Sprintf("+%s", os.Getenv("BACKUP_FILENAME"))).Output() if err != nil { return fmt.Errorf("takeBackup: error formatting filename template: %w", err) } file := fmt.Sprintf("/tmp/%s", strings.TrimSpace(string(outBytes))) 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 } func (s *script) restartContainers() error { 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{}, ) } } s.stoppedContainers = []types.Container{} return nil } func (s *script) encryptBackup() error { key := os.Getenv("GPG_PASSPHRASE") if key == "" { return nil } return errors.New("encryptBackup: not implemented yet") } func (s *script) copyBackup() error { _, 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 } func (s *script) cleanBackup() error { if err := os.Remove(s.file); err != nil { return fmt.Errorf("cleanBackup: error removing file: %w", err) } return nil } func (s *script) pruneOldBackups() error { retention := os.Getenv("BACKUP_RETENTION_DAYS") if retention == "" { return nil } sleepFor, err := time.ParseDuration(os.Getenv("BACKUP_PRUNING_LEEWAY")) if err != nil { return fmt.Errorf("pruneBackups: error parsing given leeway value: %w", err) } time.Sleep(sleepFor) if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { } if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { } return nil } func fileExists(location string) (bool, error) { _, err := os.Stat(location) if err != nil && !os.IsNotExist(err) { return false, err } return err == nil, nil } 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 } _, err = io.Copy(out, in) if err != nil { out.Close() return err } return out.Close() }