mirror of https://github.com/portainer/portainer
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
parent
d9cc7eda51
commit
9af9395b73
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
>
|
>
|
||||||
|
|
Loading…
Reference in New Issue