diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index 1304486e1..c5f2867b4 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -11,6 +11,7 @@ import ( "github.com/gofrs/uuid" portainer "github.com/portainer/portainer/api" + "github.com/rs/zerolog/log" "io" "os" @@ -316,6 +317,43 @@ func (service *Service) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileNam return service.wrapFileStore(stackStorePath), nil } +// GetEdgeStackProjectPathByVersion returns the absolute path on the FS for a edge stack based +// on its identifier and version. +// EE only feature +func (service *Service) GetEdgeStackProjectPathByVersion(edgeStackIdentifier string, version int) string { + versionStr := fmt.Sprintf("v%d", version) + return JoinPaths(service.wrapFileStore(EdgeStackStorePath), edgeStackIdentifier, versionStr) +} + +// StoreEdgeStackFileFromBytesByVersion creates a subfolder in the EdgeStackStorePath with version and stores a new file from bytes. +// It returns the path to the folder where the file is stored. +// EE only feature +func (service *Service) StoreEdgeStackFileFromBytesByVersion(edgeStackIdentifier, fileName string, version int, data []byte) (string, error) { + versionStr := fmt.Sprintf("v%d", version) + stackStorePath := JoinPaths(EdgeStackStorePath, edgeStackIdentifier, versionStr) + + err := service.createDirectoryInStore(stackStorePath) + if err != nil { + return "", err + } + + composeFilePath := JoinPaths(stackStorePath, fileName) + r := bytes.NewReader(data) + + err = service.createFileInStore(composeFilePath, r) + if err != nil { + return "", err + } + + return service.wrapFileStore(stackStorePath), nil +} + +// FormProjectPathByVersion returns the absolute path on the FS for a project based with version +func (service *Service) FormProjectPathByVersion(path string, version int) string { + versionStr := fmt.Sprintf("v%d", version) + return JoinPaths(path, versionStr) +} + // StoreRegistryManagementFileFromBytes creates a subfolder in the // ExtensionRegistryManagementStorePath and stores a new file from bytes. // It returns the path to the folder where the file is stored. @@ -734,6 +772,56 @@ func FileExists(filePath string) (bool, error) { return true, nil } +// SafeCopyDirectory copies a directory from src to dst in a safe way. +func (service *Service) SafeMoveDirectory(originalPath, newPath string) error { + // 1. Backup the source directory to a different folder + backupDir := fmt.Sprintf("%s-%s", filepath.Dir(originalPath), "backup") + err := MoveDirectory(originalPath, backupDir) + if err != nil { + return fmt.Errorf("failed to backup source directory: %w", err) + } + + defer func() { + if err != nil { + // If an error occurred, rollback the backup directory + restoreErr := restoreBackup(originalPath, backupDir) + if restoreErr != nil { + log.Warn().Err(restoreErr).Msg("failed to restore backup during creating versioning folder") + } + } + }() + + // 2. Copy the backup directory to the destination directory + err = CopyDir(backupDir, newPath, false) + if err != nil { + return fmt.Errorf("failed to copy backup directory to destination directory: %w", err) + } + + // 3. Delete the backup directory + err = os.RemoveAll(backupDir) + if err != nil { + return fmt.Errorf("failed to delete backup directory: %w", err) + } + + return nil +} + +func restoreBackup(src, backupDir string) error { + // Rollback by deleting the original directory and copying the + // backup direcotry back to the source directory, and then deleting + // the backup directory + err := os.RemoveAll(src) + if err != nil { + return fmt.Errorf("failed to delete destination directory: %w", err) + } + + err = MoveDirectory(backupDir, src) + if err != nil { + return fmt.Errorf("failed to restore backup directory: %w", err) + } + return nil +} + func MoveDirectory(originalPath, newPath string) error { if _, err := os.Stat(originalPath); err != nil { return err diff --git a/api/portainer.go b/api/portainer.go index 0e23c60c3..568b61921 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -299,6 +299,12 @@ type ( Endpoints []EndpointID `json:"Endpoints"` } + // StackDeploymentInfo records the information of a deployed stack + StackDeploymentInfo struct { + Version int `json:"Version"` + ConfigHash string `json:"ConfigHash"` + } + //EdgeStack represents an edge stack EdgeStack struct { // EdgeStack Identifier @@ -340,6 +346,8 @@ type ( Details EdgeStackStatusDetails `json:"Details"` Error string `json:"Error"` EndpointID EndpointID `json:"EndpointID"` + // EE only feature + DeploymentInfo StackDeploymentInfo `json:"DeploymentInfo"` // Deprecated Type EdgeStackStatusType `json:"Type"` @@ -1380,6 +1388,10 @@ type ( RollbackStackFile(stackIdentifier, fileName string) error GetEdgeStackProjectPath(edgeStackIdentifier string) string StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) + GetEdgeStackProjectPathByVersion(edgeStackIdentifier string, version int) string + StoreEdgeStackFileFromBytesByVersion(edgeStackIdentifier, fileName string, version int, data []byte) (string, error) + FormProjectPathByVersion(projectIdentifier string, version int) string + SafeMoveDirectory(src, dst string) error StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) KeyPairFilesExist() (bool, error) StoreKeyPair(private, public []byte, privatePEMHeader, publicPEMHeader string) error