From 7cb6e3f66a7b339936ed18f1e31dd502640566d9 Mon Sep 17 00:00:00 2001 From: cmeng Date: Fri, 23 Jun 2023 09:41:50 +1200 Subject: [PATCH] feat(edge-stack): relative path support for edge stack EE-5521 (#9103) --- api/edge/edge.go | 21 +-- api/filesystem/serialize.go | 142 ++++++++++++++++++ .../endpoint_edgestack_inspect.go | 26 ++-- 3 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 api/filesystem/serialize.go diff --git a/api/edge/edge.go b/api/edge/edge.go index ee0cac38a..ebeee170f 100644 --- a/api/edge/edge.go +++ b/api/edge/edge.go @@ -1,5 +1,7 @@ package edge +import "github.com/portainer/portainer/api/filesystem" + type ( // StackPayload represents the payload sent to the agent @@ -8,35 +10,36 @@ type ( ID int // Name of the stack Name string - // Content of the stack file - FileContent string + + // Content of stack folder + DirEntries []filesystem.DirEntry + // Name of the stack entry file + EntryFileName string // Namespace to use for kubernetes stack. Keep empty to use the manifest namespace. Namespace string // Version of the stack file Version int - // Content of the .env file - DotEnvFileContent string // RegistryCredentials holds the credentials for a Docker registry. - // // Used only for EE RegistryCredentials []RegistryCredentials // PrePullImage is a flag indicating if the agent should pull the image before deploying the stack. - // // Used only for EE PrePullImage bool // RePullImage is a flag indicating if the agent should pull the image if it is already present on the node. - // // Used only for EE RePullImage bool // RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails. - // // Used only for EE RetryDeploy bool // EdgeUpdateID is the ID of the edge update related to this stack. - // // Used only for EE EdgeUpdateID int + + // Is relative path supported + SupportRelativePath bool + // Mount point for relative path + FilesystemPath string } // RegistryCredentials holds the credentials for a Docker registry. diff --git a/api/filesystem/serialize.go b/api/filesystem/serialize.go new file mode 100644 index 000000000..e0fd5bc09 --- /dev/null +++ b/api/filesystem/serialize.go @@ -0,0 +1,142 @@ +package filesystem + +import ( + "encoding/base64" + "os" + "path/filepath" + "strings" +) + +type DirEntry struct { + Name string + Content string + IsFile bool + Permissions os.FileMode +} + +// FilterDirForEntryFile filers the given dirEntries, returns entries of the entryFile and .env file +func FilterDirForEntryFile(dirEntries []DirEntry, entryFile string) []DirEntry { + var filteredDirEntries []DirEntry + + dotEnvFile := filepath.Join(filepath.Dir(entryFile), ".env") + filters := []string{entryFile, dotEnvFile} + + for _, dirEntry := range dirEntries { + match := false + if dirEntry.IsFile { + for _, filter := range filters { + if filter == dirEntry.Name { + match = true + break + } + } + } else { + for _, filter := range filters { + if strings.HasPrefix(filter, dirEntry.Name) { + match = true + break + } + } + } + if match { + filteredDirEntries = append(filteredDirEntries, dirEntry) + } + } + + return filteredDirEntries +} + +// LoadDir reads all files and folders recursively from the given directory +// File content is base64-encoded +func LoadDir(dir string) ([]DirEntry, error) { + var dirEntries []DirEntry + + err := filepath.WalkDir( + dir, + func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + fileInfo, err := d.Info() + if err != nil { + return err + } + + relativePath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + if relativePath == "." { + return nil + } + + dirEntry := DirEntry{ + Name: relativePath, + Permissions: fileInfo.Mode().Perm(), + } + + if !fileInfo.IsDir() { + // Read file contents + fileContent, err := os.ReadFile(path) + if err != nil { + return err + } + + dirEntry.Content = base64.StdEncoding.EncodeToString(fileContent) + dirEntry.IsFile = true + } + + dirEntries = append(dirEntries, dirEntry) + return nil + }) + + if err != nil { + return nil, err + } + + return dirEntries, nil +} + +// PersistDir writes the provided array of files and folders back to the given directory. +func PersistDir(dir string, dirEntries []DirEntry) error { + for _, dirEntry := range dirEntries { + path := filepath.Join(dir, dirEntry.Name) + + if dirEntry.IsFile { + // Create the directory path if it doesn't exist + err := os.MkdirAll(filepath.Dir(path), 0744) + if err != nil { + return err + } + + // Write file contents + err = os.WriteFile(path, []byte(dirEntry.Content), dirEntry.Permissions) + if err != nil { + return err + } + } else { + // Create the directory + err := os.MkdirAll(path, dirEntry.Permissions) + if err != nil { + return err + } + } + } + + return nil +} + +func DecodeDirEntries(dirEntries []DirEntry) error { + for index, dirEntry := range dirEntries { + if dirEntry.IsFile && dirEntry.Content != "" { + decodedBytes, err := base64.StdEncoding.DecodeString(dirEntry.Content) + if err != nil { + return err + } + dirEntries[index].Content = string(decodedBytes) + } + } + + return nil +} diff --git a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go index 41b357c8e..a7071608c 100644 --- a/api/http/handler/endpointedge/endpoint_edgestack_inspect.go +++ b/api/http/handler/endpointedge/endpoint_edgestack_inspect.go @@ -2,18 +2,17 @@ package endpointedge import ( "errors" - "net/http" - "os" - "path" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/edge" + "github.com/portainer/portainer/api/filesystem" "github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/kubernetes" + "net/http" ) // @summary Inspect an Edge Stack for an Environment(Endpoint) @@ -69,26 +68,19 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http. if fileName == "" { return httperror.BadRequest("Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack")) } - } - stackFileContent, err := handler.FileService.GetFileContent(edgeStack.ProjectPath, fileName) + dirEntries, err := filesystem.LoadDir(edgeStack.ProjectPath) if err != nil { - return httperror.InternalServerError("Unable to retrieve Compose file from disk", err) + return httperror.InternalServerError("Unable to load repository", err) } - var dotEnvFileContent []byte - if _, err = os.Stat(path.Join(edgeStack.ProjectPath, ".env")); err == nil { - dotEnvFileContent, err = handler.FileService.GetFileContent(edgeStack.ProjectPath, ".env") - if err != nil { - return httperror.InternalServerError("Unable to retrieve .env file from disk", err) - } - } + dirEntries = filesystem.FilterDirForEntryFile(dirEntries, fileName) return response.JSON(w, edge.StackPayload{ - DotEnvFileContent: string(dotEnvFileContent), - FileContent: string(stackFileContent), - Name: edgeStack.Name, - Namespace: namespace, + DirEntries: dirEntries, + EntryFileName: fileName, + Name: edgeStack.Name, + Namespace: namespace, }) }