feat(edge-stack): relative path support for edge stack EE-5521 (#9103)

pull/9093/head
cmeng 2023-06-23 09:41:50 +12:00 committed by GitHub
parent 4cc96b4b30
commit 7cb6e3f66a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 163 additions and 26 deletions

View File

@ -1,5 +1,7 @@
package edge package edge
import "github.com/portainer/portainer/api/filesystem"
type ( type (
// StackPayload represents the payload sent to the agent // StackPayload represents the payload sent to the agent
@ -8,35 +10,36 @@ type (
ID int ID int
// Name of the stack // Name of the stack
Name string 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 to use for kubernetes stack. Keep empty to use the manifest namespace.
Namespace string Namespace string
// Version of the stack file // Version of the stack file
Version int Version int
// Content of the .env file
DotEnvFileContent string
// RegistryCredentials holds the credentials for a Docker registry. // RegistryCredentials holds the credentials for a Docker registry.
//
// Used only for EE // Used only for EE
RegistryCredentials []RegistryCredentials RegistryCredentials []RegistryCredentials
// PrePullImage is a flag indicating if the agent should pull the image before deploying the stack. // PrePullImage is a flag indicating if the agent should pull the image before deploying the stack.
//
// Used only for EE // Used only for EE
PrePullImage bool PrePullImage bool
// RePullImage is a flag indicating if the agent should pull the image if it is already present on the node. // RePullImage is a flag indicating if the agent should pull the image if it is already present on the node.
//
// Used only for EE // Used only for EE
RePullImage bool RePullImage bool
// RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails. // RetryDeploy is a flag indicating if the agent should retry to deploy the stack if it fails.
//
// Used only for EE // Used only for EE
RetryDeploy bool RetryDeploy bool
// EdgeUpdateID is the ID of the edge update related to this stack. // EdgeUpdateID is the ID of the edge update related to this stack.
//
// Used only for EE // Used only for EE
EdgeUpdateID int EdgeUpdateID int
// Is relative path supported
SupportRelativePath bool
// Mount point for relative path
FilesystemPath string
} }
// RegistryCredentials holds the credentials for a Docker registry. // RegistryCredentials holds the credentials for a Docker registry.

142
api/filesystem/serialize.go Normal file
View File

@ -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
}

View File

@ -2,18 +2,17 @@ package endpointedge
import ( import (
"errors" "errors"
"net/http"
"os"
"path"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/edge" "github.com/portainer/portainer/api/edge"
"github.com/portainer/portainer/api/filesystem"
"github.com/portainer/portainer/api/http/middlewares" "github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/internal/endpointutils" "github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes" "github.com/portainer/portainer/api/kubernetes"
"net/http"
) )
// @summary Inspect an Edge Stack for an Environment(Endpoint) // @summary Inspect an Edge Stack for an Environment(Endpoint)
@ -69,26 +68,19 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
if fileName == "" { if fileName == "" {
return httperror.BadRequest("Kubernetes is not supported by this stack", errors.New("Kubernetes is not supported by this stack")) 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 { 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 dirEntries = filesystem.FilterDirForEntryFile(dirEntries, fileName)
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)
}
}
return response.JSON(w, edge.StackPayload{ return response.JSON(w, edge.StackPayload{
DotEnvFileContent: string(dotEnvFileContent), DirEntries: dirEntries,
FileContent: string(stackFileContent), EntryFileName: fileName,
Name: edgeStack.Name, Name: edgeStack.Name,
Namespace: namespace, Namespace: namespace,
}) })
} }