From 9af9395b73f5b7bdfb73abf307d53cb45e8dc2cc Mon Sep 17 00:00:00 2001 From: Rex Wang <109048808+RexWangPT@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:50:59 +0800 Subject: [PATCH] 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 --- api/filesystem/filesystem.go | 77 +++++++++++++++++++ api/http/handler/stacks/stack_update.go | 33 +++++++- .../handler/stacks/update_kubernetes_stack.go | 9 ++- api/portainer.go | 3 + app/portainer/views/stacks/edit/stack.html | 2 +- 5 files changed, 120 insertions(+), 4 deletions(-) diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index 9cdbe2f5e..f0202feec 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -234,6 +234,58 @@ func (service *Service) StoreStackFileFromBytes(stackIdentifier, fileName string 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 // on its identifier. func (service *Service) GetEdgeStackProjectPath(edgeStackIdentifier string) string { @@ -447,6 +499,31 @@ func (service *Service) createFileInStore(filePath string, r io.Reader) error { 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 { path := service.wrapFileStore(filePath) block := &pem.Block{Type: fileType, Bytes: content} diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 7a29ec2c6..6bd60de74 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -1,6 +1,7 @@ package stacks import ( + "log" "net/http" "strconv" "time" @@ -189,21 +190,35 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta stack.Env = payload.Env 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 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} } config, configErr := handler.createComposeDeployConfig(r, stack, endpoint) 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 } err = handler.deployComposeStack(config, false) 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} } + handler.FileService.RemoveStackFileBackup(stackFolder, stack.EntryPoint) + return nil } @@ -226,20 +241,34 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack stack.Env = payload.Env 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 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} } config, configErr := handler.createSwarmDeployConfig(r, stack, endpoint, payload.Prune) 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 } err = handler.deploySwarmStack(config) 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} } + handler.FileService.RemoveStackFileBackup(stackFolder, stack.EntryPoint) + return nil } diff --git a/api/http/handler/stacks/update_kubernetes_stack.go b/api/http/handler/stacks/update_kubernetes_stack.go index 70f13ec04..a534e61c8 100644 --- a/api/http/handler/stacks/update_kubernetes_stack.go +++ b/api/http/handler/stacks/update_kubernetes_stack.go @@ -3,6 +3,7 @@ package stacks import ( "fmt" "io/ioutil" + "log" "net/http" "os" "strconv" @@ -124,8 +125,12 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer. } 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 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" if stack.IsComposeFormat { fileType = "Compose" @@ -135,5 +140,7 @@ func (handler *Handler) updateKubernetesStack(r *http.Request, stack *portainer. } stack.ProjectPath = projectPath + handler.FileService.RemoveStackFileBackup(stackFolder, stack.EntryPoint) + return nil } diff --git a/api/portainer.go b/api/portainer.go index b3e7e6689..ba65a96d3 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1278,6 +1278,9 @@ type ( DeleteTLSFiles(folder string) error GetStackProjectPath(stackIdentifier string) string 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 StoreEdgeStackFileFromBytes(edgeStackIdentifier, fileName string, data []byte) (string, error) StoreRegistryManagementFileFromBytes(folder, fileName string, data []byte) (string, error) diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html index b7b59d2a9..0fb82c84c 100644 --- a/app/portainer/views/stacks/edit/stack.html +++ b/app/portainer/views/stacks/edit/stack.html @@ -203,7 +203,7 @@