Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#1665. Improve Package Selection #1687

Merged
merged 26 commits into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f61319f
#1665. Update find.go to no longer recurse when looking for packages,…
mike-winberry May 9, 2023
db518df
#1665. Created file explorer and implemented logic for exploring fold…
mike-winberry May 10, 2023
e15abe1
#1665. Remove unnecessary declaration
mike-winberry May 10, 2023
eefefe2
1686. Replace Zarf Logo (#1700)
mike-winberry May 11, 2023
4373850
800. Add sbom section to deploy configure/review (#1713)
mike-winberry May 22, 2023
670cd2a
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry May 30, 2023
edf9430
#1665. Removed the explorer logic from the api and ui.
mike-winberry May 31, 2023
82eb218
#1665. Updated find and the ui local package table to stream in resul…
mike-winberry Jun 1, 2023
731c6e2
#1665. Update finding packages behavior with FindInHome that takes a …
mike-winberry Jun 1, 2023
9223de0
#1665. Updated ui local-package-table to handle the expanded search f…
mike-winberry Jun 2, 2023
3e31084
#1665. Update ui local-package-table with warning symbol if no packag…
mike-winberry Jun 2, 2023
b4bea77
#1665. Update pkg/utils io.go: Removed unused flags from RecursiveFil…
mike-winberry Jun 2, 2023
334c9ef
#1665 Update ui local-package-table expanded search button to say sea…
mike-winberry Jun 2, 2023
24a6b54
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry Jun 5, 2023
3490db8
#1665. Update ui tests with expanded search logic
mike-winberry Jun 5, 2023
f8a0d79
Fix unused var warning in api/packages find.go
mike-winberry Jun 5, 2023
197f50c
#1665. Update ui deploy_non_init to run expanded search if dos games …
mike-winberry Jun 5, 2023
ee3cf00
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry Jun 6, 2023
7252515
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry Jun 6, 2023
b4125c4
1665. Made requested changes/fixes
mike-winberry Jun 8, 2023
5b6fb4d
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry Jun 8, 2023
61705c1
Update src/internal/packager/template/yaml.go
mike-winberry Jun 12, 2023
2ce641a
Update src/pkg/packager/deploy.go
mike-winberry Jun 12, 2023
7504a78
Update src/pkg/packager/create.go
mike-winberry Jun 12, 2023
b69f7c5
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry Jun 12, 2023
4b02f4e
Merge branch 'main' into 1665-improve-package-selection-web-ui-workflow
mike-winberry Jun 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cmd/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var destroyCmd = &cobra.Command{

// Run all the scripts!
pattern := regexp.MustCompile(`(?mi)zarf-clean-.+\.sh$`)
scripts, _ := utils.RecursiveFileList(config.ZarfCleanupScriptsPath, pattern, false, true)
scripts, _ := utils.RecursiveFileList(config.ZarfCleanupScriptsPath, pattern, true)
// Iterate over all matching zarf-clean scripts and exec them
for _, script := range scripts {
// Run the matched script
Expand Down
182 changes: 155 additions & 27 deletions src/internal/api/packages/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,180 @@
package packages

import (
"encoding/json"
"fmt"
"io/fs"
"net/http"
"os"
"path/filepath"
"regexp"

"github.com/defenseunicorns/zarf/src/internal/api/common"
"github.com/defenseunicorns/zarf/src/pkg/message"
"github.com/defenseunicorns/zarf/src/config"
"github.com/defenseunicorns/zarf/src/pkg/packager"
"github.com/defenseunicorns/zarf/src/pkg/utils"
)

// Find zarf-packages on the local system (https://regex101.com/r/TUUftK/1)
var packagePattern = regexp.MustCompile(`zarf-package[^\s\\\/]*\.tar(\.zst)?$`)
// Find zarf-init packages on the local system (https://regex101.com/r/6aTl3O/2)
var initPattern = regexp.MustCompile(`zarf-init[^\s\\\/]*\.tar(\.zst)?$`)

// Find returns all packages anywhere down the directory tree of the working directory.
func Find(w http.ResponseWriter, _ *http.Request) {
message.Debug("packages.Find()")
findPackage(packagePattern, w, os.Getwd)
// Find zarf-init packages on the local system
var currentInitPattern = regexp.MustCompile(packager.GetInitPackageName("") + "$")

// FindInHomeStream returns all packages in the user's home directory.
// If the init query parameter is true, only init packages will be returned.
func FindInHomeStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

init := r.URL.Query().Get("init")
regexp := packagePattern
if init == "true" {
regexp = currentInitPattern
}

done := make(chan bool)
go func() {
// User home directory
homePath, err := os.UserHomeDir()
if err != nil {
streamError(err, w)
} else {
// Recursively search for and stream packages in the home directory
recursivePackageStream(homePath, regexp, w)
}
close(done)
}()

<-done
}

// FindInHome returns all packages in the user's home directory.
func FindInHome(w http.ResponseWriter, _ *http.Request) {
message.Debug("packages.FindInHome()")
findPackage(packagePattern, w, os.UserHomeDir)
// FindInitStream finds and streams all init packages in the current working directory, the cache directory, and execution directory
func FindInitStream(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")

done := make(chan bool)
go func() {
// stream init packages in the execution directory
if execDir, err := utils.GetFinalExecutablePath(); err == nil {
streamDirPackages(execDir, currentInitPattern, w)
} else {
streamError(err, w)
}

// Cache directory
cachePath := config.GetAbsCachePath()
// Create the cache directory if it doesn't exist
if utils.InvalidPath(cachePath) {
if err := os.MkdirAll(cachePath, 0755); err != nil {
streamError(err, w)
}
}
streamDirPackages(cachePath, currentInitPattern, w)

// Find init packages in the current working directory
if cwd, err := os.Getwd(); err == nil {
streamDirPackages(cwd, currentInitPattern, w)
} else {
streamError(err, w)
}
close(done)
}()
<-done
}

// FindInitPackage returns all init packages anywhere down the directory tree of the users home directory.
func FindInitPackage(w http.ResponseWriter, _ *http.Request) {
message.Debug("packages.FindInitPackage()")
findPackage(initPattern, w, os.UserHomeDir)
// FindPackageStream finds and streams all packages in the current working directory
func FindPackageStream(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
done := make(chan bool)

go func() {
if cwd, err := os.Getwd(); err == nil {
streamDirPackages(cwd, packagePattern, w)
} else {
streamError(err, w)
}
close(done)
}()

<-done
// Find init packages in the current working directory
}

func findPackage(pattern *regexp.Regexp, w http.ResponseWriter, setDir func() (string, error)) {
targetDir, err := setDir()
// recursivePackageStream recursively searches for and streams packages in the given directory
func recursivePackageStream(dir string, pattern *regexp.Regexp, w http.ResponseWriter) {
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
// ignore files/dirs that it does not have permission to read
if err != nil && os.IsPermission(err) {
return nil
}

// Return error if the pattern is invalid
if pattern == nil {
return filepath.ErrBadPattern
}

// Return errors
if err != nil {
return err
}

if !d.IsDir() {
if len(pattern.FindStringIndex(path)) > 0 {
streamPackage(path, w)
}
// Skip the trash bin and hidden directories
} else if utils.IsTrashBin(path) {
return filepath.SkipDir
}

return nil
})
if err != nil {
message.ErrorWebf(err, w, "Error getting directory")
return
streamError(err, w)
}
}

// Skip permission errors, search dot-prefixed directories.
files, err := utils.RecursiveFileList(targetDir, pattern, true, false)
if err != nil || len(files) == 0 {
pkgNotFoundMsg := fmt.Sprintf("Unable to locate the package: %s", pattern.String())
message.ErrorWebf(err, w, pkgNotFoundMsg)
return
// streamDirPackages streams all packages in the given directory
func streamDirPackages(dir string, pattern *regexp.Regexp, w http.ResponseWriter) {
files, err := os.ReadDir(dir)
if err != nil {
streamError(err, w)
}
for _, file := range files {
if !file.IsDir() {
path := fmt.Sprintf("%s/%s", dir, file.Name())
if pattern != nil {
if len(pattern.FindStringIndex(path)) > 0 {
streamPackage(path, w)
}
}
}
}
common.WriteJSONResponse(w, files, http.StatusOK)
}

// streamPackage streams the package at the given path
func streamPackage(path string, w http.ResponseWriter) {
pkg, err := ReadPackage(path)
if err != nil {
streamError(err, w)
} else {
jsonData, err := json.Marshal(pkg)
if err != nil {
streamError(err, w)
} else {
fmt.Fprintf(w, "data: %s\n\n", jsonData)
w.(http.Flusher).Flush()
}
}
}

// streamError streams the given error to the client
func streamError(err error, w http.ResponseWriter) {
fmt.Fprintf(w, "data: %s\n\n", err.Error())
w.(http.Flusher).Flush()
}
6 changes: 3 additions & 3 deletions src/internal/api/packages/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ func Read(w http.ResponseWriter, r *http.Request) {

path := chi.URLParam(r, "path")

if pkg, err := readPackage(path); err != nil {
if pkg, err := ReadPackage(path); err != nil {
message.ErrorWebf(err, w, "Unable to read the package at: `%s`", path)
} else {
common.WriteJSONResponse(w, pkg, http.StatusOK)
}
}

// internal function to read a package from the local filesystem.
func readPackage(path string) (pkg types.APIZarfPackage, err error) {
// ReadPackage reads a packages yaml from the local filesystem and returns an APIZarfPackage.
func ReadPackage(path string) (pkg types.APIZarfPackage, err error) {
var file []byte

pkg.Path, err = url.QueryUnescape(path)
Expand Down
10 changes: 7 additions & 3 deletions src/internal/api/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,6 @@ func LaunchAPIServer() {
})

r.Route("/packages", func(r chi.Router) {
r.Get("/find", packages.Find)
r.Get("/find-in-home", packages.FindInHome)
r.Get("/find-init", packages.FindInitPackage)
r.Get("/read/{path}", packages.Read)
r.Get("/list", packages.ListDeployedPackages)
r.Put("/deploy", packages.DeployPackage)
Expand All @@ -100,6 +97,13 @@ func LaunchAPIServer() {
r.Get("/connections", packages.ListConnections)
r.Get("/sbom/{path}", packages.ExtractSBOM)
r.Delete("/sbom", packages.DeleteSBOM)
r.Route("/find", func(r chi.Router) {
r.Route("/stream", func(r chi.Router) {
r.Get("/", packages.FindPackageStream)
r.Get("/init", packages.FindInitStream)
r.Get("/home", packages.FindInHomeStream)
})
})
})

r.Route("/components", func(r chi.Router) {
Expand Down
2 changes: 1 addition & 1 deletion src/internal/packager/template/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
func ProcessYamlFilesInPath(path string, component types.ZarfComponent, values Values) ([]string, error) {
// Only pull in yml and yaml files
pattern := regexp.MustCompile(`(?mi)\.ya?ml$`)
manifests, _ := utils.RecursiveFileList(path, pattern, false, true)
manifests, _ := utils.RecursiveFileList(path, pattern, false)

for _, manifest := range manifests {
if err := values.Apply(component, manifest, false); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/packager/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func (p *Packager) getFilesToSBOM(component types.ZarfComponent) (*types.Compone

appendSBOMFiles := func(path string) {
if utils.IsDir(path) {
files, _ := utils.RecursiveFileList(path, nil, false, true)
files, _ := utils.RecursiveFileList(path, nil, false)
componentSBOM.Files = append(componentSBOM.Files, files...)
} else {
componentSBOM.Files = append(componentSBOM.Files, path)
Expand Down
2 changes: 1 addition & 1 deletion src/pkg/packager/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ func (p *Packager) processComponentFiles(component types.ZarfComponent, pkgLocat

fileList := []string{}
if utils.IsDir(fileLocation) {
files, _ := utils.RecursiveFileList(fileLocation, nil, false, true)
files, _ := utils.RecursiveFileList(fileLocation, nil, false)
fileList = append(fileList, files...)
} else {
fileList = append(fileList, fileLocation)
Expand Down
45 changes: 32 additions & 13 deletions src/pkg/utils/io.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,20 +168,9 @@ func ReplaceTextTemplate(path string, mappings map[string]*TextTemplate, depreca
}

// RecursiveFileList walks a path with an optional regex pattern and returns a slice of file paths.
// the skipPermission flag can be provided to ignore unauthorized files/dirs when true
// the skipHidden flag can be provided to ignore dot prefixed files/dirs when true
func RecursiveFileList(dir string, pattern *regexp.Regexp, skipPermission bool, skipHidden bool) (files []string, err error) {
// If skipHidden is true, hidden directories will be skipped.
func RecursiveFileList(dir string, pattern *regexp.Regexp, skipHidden bool) (files []string, err error) {
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
// ignore files/dirs that it does not have permission to read
if err != nil && os.IsPermission(err) && skipPermission {
return nil
}
// Skip hidden directories
if skipHidden {
if d.IsDir() && d.Name()[0] == dotCharacter {
return filepath.SkipDir
}
}

// Return errors
if err != nil {
Expand All @@ -196,6 +185,9 @@ func RecursiveFileList(dir string, pattern *regexp.Regexp, skipPermission bool,
} else {
files = append(files, path)
}
// Skip hidden directories
} else if skipHidden && IsHidden(d.Name()) {
return filepath.SkipDir
}

return nil
Expand Down Expand Up @@ -289,6 +281,33 @@ func IsTextFile(path string) (bool, error) {
return hasText || hasJSON || hasXML, nil
}

// IsTrashBin checks if the given directory path corresponds to an operating system's trash bin.
func IsTrashBin(dirPath string) bool {
dirPath = filepath.Clean(dirPath)

// Check if the directory path matches a Linux trash bin
if strings.HasSuffix(dirPath, "/Trash") || strings.HasSuffix(dirPath, "/.Trash-1000") {
return true
}

// Check if the directory path matches a macOS trash bin
if strings.HasSuffix(dirPath, "./Trash") || strings.HasSuffix(dirPath, "/.Trashes") {
return true
}

// Check if the directory path matches a Windows trash bin
if strings.HasSuffix(dirPath, "\\$RECYCLE.BIN") {
return true
}

return false
}

// IsHidden returns true if the given file name starts with a dot.
func IsHidden(name string) bool {
return name[0] == dotCharacter
}

// GetDirSize walks through all files and directories in the provided path and returns the total size in bytes.
func GetDirSize(path string) (int64, error) {
dirSize := int64(0)
Expand Down
12 changes: 11 additions & 1 deletion src/test/ui/02_initialize_cluster.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ test.beforeEach(async ({ page }) => {
page.on('pageerror', (err) => console.log(err.message));
});

const expandedSearch = async (page) => {
const expanded = (await page.locator('.button-label:has-text("Search Directory")')).first();
if (expanded.isVisible()) {
await expanded.click();
}
};

const getToSelectPage = async (page) => {
await page.goto('/auth?token=insecure&next=/packages?init=true', { waitUntil: 'networkidle' });
};

const getToConfigurePage = async (page) => {
await getToSelectPage(page);
await expandedSearch(page);
// Find first init package deploy button.
const deployInit = page.getByTitle('init').first();
// click the init package deploy button.
Expand Down Expand Up @@ -42,8 +50,10 @@ test.describe('initialize a zarf cluster', () => {
'4 Deploy',
]);

await expandedSearch(page);

// Find first init package deploy button.
const deployInit = page.getByTitle('init').first();
let deployInit = page.getByTitle('init').first();
// click the init package deploy button.
deployInit.click();

Expand Down
4 changes: 4 additions & 0 deletions src/test/ui/03_deploy_non_init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const getToSelectPage = async (page) => {

const getToReview = async (page) => {
await getToSelectPage(page);
const expanded = (await page.locator('.button-label:has-text("Search Directory")')).first();
if (expanded.isVisible()) {
await expanded.click();
}
// Find first dos-games package deploy button.
const dosGames = page.getByTitle('dos-games').first();
// click the dos-games package deploy button.
Expand Down
Loading