Adam Štrauch
872826c0b1
All checks were successful
continuous-integration/drone/push Build is passing
Three days expiration
370 lines
10 KiB
Go
370 lines
10 KiB
Go
package apps
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mholt/archiver/v3"
|
|
"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"
|
|
|
|
// 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) {
|
|
// Create an archive
|
|
archive := archiver.TarZstd{
|
|
Tar: &archiver.Tar{
|
|
MkdirAll: true,
|
|
ContinueOnError: true,
|
|
OverwriteExisting: false,
|
|
ImplicitTopLevelFolder: false,
|
|
},
|
|
// CompressionLevel: 6,
|
|
// SelectiveCompression: true,
|
|
}
|
|
|
|
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 = archive.Archive([]string{"./"}, tmpSnapshotArchivePath)
|
|
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)
|
|
}
|
|
|
|
archive := archiver.TarZstd{
|
|
Tar: &archiver.Tar{
|
|
MkdirAll: true,
|
|
ContinueOnError: true,
|
|
OverwriteExisting: false,
|
|
ImplicitTopLevelFolder: false,
|
|
},
|
|
// CompressionLevel: 6,
|
|
// SelectiveCompression: true,
|
|
}
|
|
|
|
err = archive.Unarchive(tmpSnapshotArchivePath, "./")
|
|
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 {
|
|
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.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
|
|
}
|