350 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | 
						|
}
 |