node-api/apps/snapshots.go
Adam Štrauch b15e85474e
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
tarBin
2022-07-23 00:36:57 +02:00

350 lines
10 KiB
Go

package apps
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"os/exec"
"path"
"strings"
"time"
"github.com/rosti-cz/node-api/apps/drivers"
uuid "github.com/satori/go.uuid"
)
const bucketName = "snapshots"
const dateFormat = "20060102_150405"
const keySplitCharacter = ":"
const metadataPrefix = "_metadata"
const metadataKeyTemplate = metadataPrefix + "/%s"
const tarBin = "/bin/tar"
// Snapshot contains metadata about a single snapshot
type Snapshot struct {
UUID string `json:"uuid"`
AppName string `json:"app_name"`
TimeStamp int64 `json:"ts"`
Labels []string `json:"labels"`
}
// SnapshotIndexLine is struct holding information about a single snapshot
func (s *Snapshot) ToString() string {
// Ignoring this error is intentional. There shouldn't be any problem with this, because all data types are ready to be marshaled.
body, _ := json.Marshal(s)
return string(body)
}
// KeyName returns keyname used to store the snapshot in the storage
func (s *Snapshot) KeyName(indexLabel string) string {
labelValue := "-"
for _, label := range s.Labels {
if strings.HasPrefix(label, indexLabel+":") {
labelValue = label[len(indexLabel)+1:]
}
}
return fmt.Sprintf("%s%s%s%s%s%s%d", s.AppName, keySplitCharacter, s.UUID, keySplitCharacter, labelValue, keySplitCharacter, s.TimeStamp)
}
type Snapshots []Snapshot
// SnapshotProcessor encapsulates everything realted to snapshots. Snapshot is an archive of app's
// directory content. It's stored in S3.
// The biggest problem in the implementation is speed of looking for snapshots by labels.
// So we can setup one label that can be used as index and it will speed up filtering a little bit in
// certain situations - all snapshot of one owner for example.
// This is distributed interface for the snapshot storage and any node can handle the request message
// so we don't have any locking mechanism here and we cannot created a single index of snapshots without
// significant time spend on it. Let's deal with it later. I think we are fine for first 10k snapshots.
type SnapshotProcessor struct {
AppsPath string // Where apps are stored
TmpSnapshotPath string // where temporary location for snapshots is
IndexLabel string // Label that will be used as index to make listing faster
Driver drivers.DriverInterface
}
// saveMetadata saves metadata of single snapshot into the metadata storage
func (s *SnapshotProcessor) saveMetadata(snapshot Snapshot) error {
body, err := json.Marshal(snapshot)
if err != nil {
return fmt.Errorf("marshal snapshot into JSON error: %v", err)
}
err = s.Driver.Write(fmt.Sprintf(metadataKeyTemplate, snapshot.UUID), body)
if err != nil {
return fmt.Errorf("copying metadata into S3 error: %v", err)
}
return nil
}
// loadMetadata returns metadata for given snapshot's UUID
func (s *SnapshotProcessor) loadMetadata(snapshotUUID string) (Snapshot, error) {
snapshot := Snapshot{}
body, err := s.Driver.Read(fmt.Sprintf(metadataKeyTemplate, snapshotUUID))
if err != nil {
return snapshot, fmt.Errorf("reading metadata from S3 error: %v", err)
}
err = json.Unmarshal(body, &snapshot)
if err != nil {
return snapshot, fmt.Errorf("decoding metadata from JSON error: %v", err)
}
return snapshot, nil
}
// deleteMetadata deletes metadata object
func (s *SnapshotProcessor) deleteMetadata(snapshotUUID string) error {
err := s.Driver.Delete(fmt.Sprintf(metadataKeyTemplate, snapshotUUID))
if err != nil {
return fmt.Errorf("delete metadata from S3 error: %v", err)
}
return nil
}
// metadataForSnapshotKey returns metadata for snapshot key
func (s *SnapshotProcessor) metadataForSnapshotKey(snapshotKey string) (Snapshot, error) {
parts := strings.Split(snapshotKey, keySplitCharacter)
if len(parts) != 4 {
return Snapshot{}, errors.New("wrong snapshot key format")
}
snapshot, err := s.loadMetadata(parts[1])
return snapshot, err
}
// CreateSnapshot creates an archive of existing application and stores it in S3 storage
// Returns key under which is the snapshot stored and/or error if there is any.
// Metadata about the snapshot are stored in extra object under metadata/ prefix.
func (s *SnapshotProcessor) CreateSnapshot(appName string, labels []string) (string, error) {
snapshot := Snapshot{
UUID: uuid.NewV4().String(),
AppName: appName,
TimeStamp: time.Now().Unix(),
Labels: labels,
}
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, snapshot.KeyName(s.IndexLabel)+".tar.zst")
err := os.Chdir(path.Join(s.AppsPath, appName))
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("change working directory error: %v", err)
}
err = exec.Command(tarBin, "-acf", tmpSnapshotArchivePath, "./").Run()
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("compression error: %v", err)
}
info, err := os.Stat(tmpSnapshotArchivePath)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("temporary file stat error: %v", err)
}
snapshot.Labels = append(snapshot.Labels, fmt.Sprintf("size:%d", info.Size()))
err = s.saveMetadata(snapshot)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("saving metadata error: %v", err)
}
// Clean after myself
defer func() {
err = os.Remove(tmpSnapshotArchivePath)
if err != nil {
log.Println("removing temporary snapshot file error:", err.Error())
}
}()
// Pipe it into the storage
err = s.Driver.Create(snapshot.KeyName(s.IndexLabel), tmpSnapshotArchivePath)
if err != nil {
return snapshot.KeyName(s.IndexLabel), fmt.Errorf("copying snapshot into S3 error: %v", err)
}
return snapshot.KeyName(s.IndexLabel), nil
}
// RestoreSnapshot restores snapshot into an existing application
// If you need a new app from existing snapshot just create it.
// This restores only content on the disk, doesn't create the container.
func (s *SnapshotProcessor) RestoreSnapshot(key string, newAppName string) error {
tmpSnapshotArchivePath := path.Join(s.TmpSnapshotPath, key+".tar.zst")
err := os.MkdirAll(path.Join(s.AppsPath, newAppName), 0755)
if err != nil {
return fmt.Errorf("creating destination path error: %v", err)
}
err = os.Chdir(path.Join(s.AppsPath, newAppName))
if err != nil {
return fmt.Errorf("creating destination path error: %v", err)
}
err = s.Driver.Get(key, tmpSnapshotArchivePath)
if err != nil {
return fmt.Errorf("getting the archive from S3 error: %v", err)
}
err = exec.Command(tarBin, "-axf", tmpSnapshotArchivePath).Run()
if err != nil {
return fmt.Errorf("unarchiving error: %v", err)
}
err = os.Remove(tmpSnapshotArchivePath)
if err != nil {
return fmt.Errorf("removing the archive error: %v", err)
}
// remove .chowned file to tell the container to setup ownership of the files again
err = os.Remove("./.chowned")
if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
return fmt.Errorf("removing error: %v", err)
}
return nil
}
// ListAppSnapshots returns list of all snapshots related to a single application
func (s *SnapshotProcessor) ListAppSnapshots(appName string) ([]Snapshot, error) {
snapshots := []Snapshot{}
keys, err := s.Driver.List("")
if err != nil {
return snapshots, err
}
for _, key := range keys {
if key == metadataPrefix+"/" {
continue
}
if strings.HasPrefix(key, appName+keySplitCharacter) {
snapshot, err := s.metadataForSnapshotKey(key)
if err != nil {
return snapshots, err
}
snapshots = append(snapshots, snapshot)
}
}
return snapshots, nil
}
// ListAppsSnapshots returns list of snapshots for all given apps
func (s *SnapshotProcessor) ListAppsSnapshots(appNames []string) ([]Snapshot, error) {
snapshots := []Snapshot{}
keys, err := s.Driver.List("")
if err != nil {
return snapshots, err
}
for _, key := range keys {
if key == metadataPrefix+"/" {
continue
}
for _, appName := range appNames {
if strings.HasPrefix(key, appName+keySplitCharacter) {
snapshot, err := s.metadataForSnapshotKey(key)
if err != nil {
return snapshots, err
}
snapshots = append(snapshots, snapshot)
}
}
}
return snapshots, nil
}
// ListAppsSnapshotsByLabel returns list of snapshots with given label
// TODO: this will be ok for now but probably little slow when users start using it more
func (s *SnapshotProcessor) ListAppsSnapshotsByLabel(labelValue string) ([]Snapshot, error) {
snapshots := []Snapshot{}
keys, err := s.Driver.List("")
if err != nil {
return snapshots, err
}
for _, key := range keys {
if key == metadataPrefix+"/" {
continue
}
snapshot, err := s.metadataForSnapshotKey(key)
if err != nil && strings.Contains(err.Error(), "wrong snapshot key format") {
log.Printf("WARNING: Snapshot storage: invalid key found (%s)", key)
continue
} else if err != nil {
return snapshots, err
}
for _, label := range snapshot.Labels {
if label == fmt.Sprintf("%s:%s", s.IndexLabel, labelValue) {
snapshots = append(snapshots, snapshot)
}
}
}
return snapshots, nil
}
// GetSnapshot returns a single snapshot's metadata for given key.
func (s *SnapshotProcessor) GetSnapshot(key string) (Snapshot, error) {
snapshot := Snapshot{}
snapshot, err := s.metadataForSnapshotKey(key)
if err != nil {
return snapshot, err
}
return snapshot, nil
}
// GetDownloadLink returns an URL for given snapshot
func (s *SnapshotProcessor) GetDownloadLink(key string) (string, error) {
link, err := s.Driver.GetDownloadLink(key)
if err != nil {
return link, err
}
return link, nil
}
// DeleteSnapshot delete's one snapshot
func (s *SnapshotProcessor) DeleteSnapshot(key string) error {
err := s.Driver.Delete(key)
if err != nil {
return fmt.Errorf("removing snapshot error: %v", err)
}
return nil
}
// DeleteAppSnapshots deletes all snapshots related to a single application
func (s *SnapshotProcessor) DeleteAppSnapshots(appName string) error {
snapshots, err := s.ListAppSnapshots(appName)
if err != nil {
return fmt.Errorf("removing snapshots error: %v", err)
}
for _, snapshot := range snapshots {
err = s.DeleteSnapshot(snapshot.KeyName(s.IndexLabel))
if err != nil {
return fmt.Errorf("removing snapshots error: %v", err)
}
}
return nil
}