fix(docker): prevent misconfigured stack from saving EE-3235 (#7585)

* EE-3235 fix(docker): add checker to editor

* support rollback to update stack file

Co-authored-by: chaogeng77977 <chao.geng@portainer.io>
pull/7627/head
Rex Wang 2022-09-07 16:50:59 +08:00 committed by GitHub
parent d9cc7eda51
commit 9af9395b73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 4 deletions

View File

@ -234,6 +234,58 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string
return service.wrapFileStore(stackStorePath), nil return service.wrapFileStore(stackStorePath), nil
} }
// UpdateStoreStackFileFromBytes makes stack file backup and updates a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) UpdateStoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) {
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
composeFilePath := JoinPaths(stackStorePath, fileName)
err := service.createBackupFileInStore(composeFilePath)
if err != nil {
return "", err
}
r := bytes.NewReader(data)
err = service.createFileInStore(composeFilePath, r)
if err != nil {
return "", err
}
return service.wrapFileStore(stackStorePath), nil
}
// RemoveStackFileBackup removes the stack file backup in the ComposeStorePath.
func (service *Service) RemoveStackFileBackup(stackIdentifier, fileName string) error {
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
composeFilePath := JoinPaths(stackStorePath, fileName)
return service.removeBackupFileInStore(composeFilePath)
}
// RollbackStackFile rollbacks the stack file backup in the ComposeStorePath.
func (service *Service) RollbackStackFile(stackIdentifier, fileName string) error {
stackStorePath := JoinPaths(ComposeStorePath, stackIdentifier)
composeFilePath := JoinPaths(stackStorePath, fileName)
path := service.wrapFileStore(composeFilePath)
backupPath := fmt.Sprintf("%s.bak", path)
exists, err := service.FileExists(backupPath)
if err != nil {
return err
}
if !exists {
// keep the updated/failed stack file
return nil
}
err = service.Copy(backupPath, path, true)
if err != nil {
return err
}
return os.Remove(backupPath)
}
// GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based // GetEdgeStackProjectPath returns the absolute path on the FS for a edge stack based
// on its identifier. // on its identifier.
func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string { func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string {
@ -447,6 +499,31 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error {
return err return err
} }
// createBackupFileInStore makes a copy in the file store.
func (service *Service) createBackupFileInStore(filePath string) error {
path := service.wrapFileStore(filePath)
backupPath := fmt.Sprintf("%s.bak", path)
return service.Copy(path, backupPath, true)
}
// removeBackupFileInStore removes the copy in the file store.
func (service *Service) removeBackupFileInStore(filePath string) error {
path := service.wrapFileStore(filePath)
backupPath := fmt.Sprintf("%s.bak", path)
exists, err := service.FileExists(backupPath)
if err != nil {
return err
}
if exists {
return os.Remove(backupPath)
}
return nil
}
func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error { func (service *Service) createPEMFileInStore(content []byte, fileType, filePath string) error {
path := service.wrapFileStore(filePath) path := service.wrapFileStore(filePath)
block := &pem.Block{Type: fileType, Bytes: content} block := &pem.Block{Type: fileType, Bytes: content}

View File

@ -1,6 +1,7 @@
package stacks package stacks
import ( import (
"log"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -189,21 +190,35 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
stack.Env = payload.Env stack.Env = payload.Env
stackFolder := strconv.Itoa(int(stack.ID)) stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) _, err = handler.FileService.UpdateStoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil { if err != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
} }
config, configErr := handler.createComposeDeployConfig(r, stack, endpoint) config, configErr := handler.createComposeDeployConfig(r, stack, endpoint)
if configErr != nil { if configErr != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
return configErr return configErr
} }
err = handler.deployComposeStack(config, false) err = handler.deployComposeStack(config, false)
if err != nil { if err != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
} }
handler.FileService.RemoveStackFileBackup(stackFolder, stack.EntryPoint)
return nil return nil
} }
@ -226,20 +241,34 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
stack.Env = payload.Env stack.Env = payload.Env
stackFolder := strconv.Itoa(int(stack.ID)) stackFolder := strconv.Itoa(int(stack.ID))
_, err = handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) _, err = handler.FileService.UpdateStoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil { if err != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [swarm,stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist updated Compose file on disk", Err: err}
} }
config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune) config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune)
if configErr != nil { if configErr != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [swarm,stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
return configErr return configErr
} }
err = handler.deploySwarmStack(config) err = handler.deploySwarmStack(config)
if err != nil { if err != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [swarm,stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err} return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: err.Error(), Err: err}
} }
handler.FileService.RemoveStackFileBackup(stackFolder, stack.EntryPoint)
return nil return nil
} }

View File

@ -3,6 +3,7 @@ package stacks
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
@ -124,8 +125,12 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
} }
stackFolder := strconv.Itoa(int(stack.ID)) stackFolder := strconv.Itoa(int(stack.ID))
projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent)) projectPath, err := handler.FileService.UpdateStoreStackFileFromBytes(stackFolder, stack.EntryPoint, []byte(payload.StackFileContent))
if err != nil { if err != nil {
if rollbackErr := handler.FileService.RollbackStackFile(stackFolder, stack.EntryPoint); rollbackErr != nil {
log.Printf("[WARN] [kubernetes,stack,update] [message: rollback stack file error] [err: %s]", rollbackErr)
}
fileType := "Manifest" fileType := "Manifest"
if stack.IsComposeFormat { if stack.IsComposeFormat {
fileType = "Compose" fileType = "Compose"
@ -135,5 +140,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer.
} }
stack.ProjectPath = projectPath stack.ProjectPath = projectPath
handler.FileService.RemoveStackFileBackup(stackFolder, stack.EntryPoint)
return nil return nil
} }

View File

@ -1278,6 +1278,9 @@ type (
DeleteTLSFiles(folder string) error DeleteTLSFiles(folder string) error
GetStackProjectPath(stackIdentifier string) string GetStackProjectPath(stackIdentifier string) string
StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error) StoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error)
UpdateStoreStackFileFromBytes(stackIdentifier, fileName string, data []byte) (string, error)
RemoveStackFileBackup(stackIdentifier, fileName string) error
RollbackStackFile(stackIdentifier, fileName string) error
GetEdgeStackProjectPath(edgeStackIdentifier string) string GetEdgeStackProjectPath(edgeStackIdentifier string) string
StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error)
StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error)

View File

@ -203,7 +203,7 @@
<button <button
type="button" type="button"
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
ng-disabled="state.actionInProgress || !stackUpdateForm.$valid || !stackFileContent || orphaned" ng-disabled="state.actionInProgress || state.yamlError || !stackUpdateForm.$valid || !stackFileContent || orphaned"
ng-click="deployStack()" ng-click="deployStack()"
button-spinner="state.actionInProgress" button-spinner="state.actionInProgress"
> >