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 }