diff --git a/.vscode/cspell.global.yaml b/.vscode/cspell.global.yaml index 3d7c79ce959..94ec7df49b0 100644 --- a/.vscode/cspell.global.yaml +++ b/.vscode/cspell.global.yaml @@ -74,6 +74,7 @@ ignoreWords: - fdfp - fics - Frontdoor + - funcignore - gitcli - golobby - graphsdk @@ -162,6 +163,7 @@ ignoreWords: - vwan - wafrg - westus + - webappignore - Wans - apim - Retryable diff --git a/cli/azd/pkg/dotignore/dotignore.go b/cli/azd/pkg/dotignore/dotignore.go index 39080f7e123..b0296862815 100644 --- a/cli/azd/pkg/dotignore/dotignore.go +++ b/cli/azd/pkg/dotignore/dotignore.go @@ -4,123 +4,38 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/denormal/go-gitignore" ) -// ReadIgnoreFiles reads all ignore files (default to ".zipignore") in the directory hierarchy, -// from the projectDir upwards, and returns a slice of gitignore.GitIgnore structures. -func ReadIgnoreFiles(projectDir string, ignoreFileName ...string) ([]gitignore.GitIgnore, error) { - var ignoreMatchers []gitignore.GitIgnore - - // Set default ignore file name to ".zipignore" if none is provided - fileName := ".zipignore" - if len(ignoreFileName) > 0 && ignoreFileName[0] != "" { - fileName = ignoreFileName[0] +// ReadDotIgnoreFile reads the ignore file located at the root of the project directory. +// If the ignoreFileName is blank or the file is not found, it returns nil, nil. +func ReadDotIgnoreFile(projectDir string, ignoreFileName string) (gitignore.GitIgnore, error) { + // Return nil if the ignoreFileName is empty + if ignoreFileName == "" { + return nil, nil } - // Traverse upwards from the projectDir to the root directory - currentDir := projectDir - for { - ignoreFilePath := filepath.Join(currentDir, fileName) - if _, err := os.Stat(ignoreFilePath); !os.IsNotExist(err) { - ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath) - if err != nil { - return nil, fmt.Errorf("error reading %s file at %s: %w", fileName, ignoreFilePath, err) - } - ignoreMatchers = append([]gitignore.GitIgnore{ignoreMatcher}, ignoreMatchers...) - } + ignoreFilePath := filepath.Join(projectDir, ignoreFileName) + if _, err := os.Stat(ignoreFilePath); os.IsNotExist(err) { + // Return nil if the ignore file does not exist + return nil, nil + } - // Stop if we've reached the root directory - parentDir := filepath.Dir(currentDir) - if parentDir == currentDir { - break - } - currentDir = parentDir + ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath) + if err != nil { + return nil, fmt.Errorf("error reading %s file at %s: %w", ignoreFileName, ignoreFilePath, err) } - return ignoreMatchers, nil + return ignoreMatcher, nil } -// ShouldIgnore checks if a file or directory should be ignored based on a slice of gitignore.GitIgnore structures. -func ShouldIgnore(path string, isDir bool, ignoreMatchers []gitignore.GitIgnore) bool { - for _, matcher := range ignoreMatchers { - match := matcher.Relative(path, isDir) - if match != nil && match.Ignore() { +// ShouldIgnore determines whether a file or directory should be ignored based on the provided ignore matcher. +func ShouldIgnore(relativePath string, isDir bool, ignoreMatcher gitignore.GitIgnore) bool { + if ignoreMatcher != nil { + if match := ignoreMatcher.Relative(relativePath, isDir); match != nil && match.Ignore() { return true } } return false } - -// RemoveIgnoredFiles removes files and directories based on ignore rules using a pre-collected list of paths. -func RemoveIgnoredFiles(staging string, ignoreMatchers []gitignore.GitIgnore) error { - if len(ignoreMatchers) == 0 { - return nil // No ignore files, no files to ignore - } - - // Collect all file and directory paths - paths, err := CollectFilePaths(staging) - if err != nil { - return fmt.Errorf("collecting file paths: %w", err) - } - - // Map to store directories that should be ignored, preventing their children from being processed - ignoredDirs := make(map[string]struct{}) - - // Iterate through collected paths and determine which to remove - for _, path := range paths { - relativePath, err := filepath.Rel(staging, path) - if err != nil { - return err - } - - // Skip processing if the path is within an ignored directory - skip := false - for ignoredDir := range ignoredDirs { - if strings.HasPrefix(relativePath, ignoredDir) { - skip = true - break - } - } - if skip { - continue - } - - isDir := false - info, err := os.Lstat(path) - if err == nil { - isDir = info.IsDir() - } - - // Check if the file should be ignored - if ShouldIgnore(relativePath, isDir, ignoreMatchers) { - if isDir { - ignoredDirs[relativePath] = struct{}{} - if err := os.RemoveAll(path); err != nil { - return fmt.Errorf("removing directory %s: %w", path, err) - } - } else { - if err := os.Remove(path); err != nil { - return fmt.Errorf("removing file %s: %w", path, err) - } - } - } - } - - return nil -} - -// CollectFilePaths collects all file and directory paths under the given root directory. -func CollectFilePaths(root string) ([]string, error) { - var paths []string - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - paths = append(paths, path) - return nil - }) - return paths, err -} diff --git a/cli/azd/pkg/project/framework_service_npm.go b/cli/azd/pkg/project/framework_service_npm.go index 31731b05583..ff97c8c95a4 100644 --- a/cli/azd/pkg/project/framework_service_npm.go +++ b/cli/azd/pkg/project/framework_service_npm.go @@ -131,10 +131,10 @@ func (np *npmProject) Package( packageSource, packageDest, buildForZipOptions{ - excludeCallback: func(src string) ([]excludeDirEntryCondition, error) { - return []excludeDirEntryCondition{excludeNodeModules}, nil - }, - }); err != nil { + excludeConditions: []excludeDirEntryCondition{excludeNodeModules}, + }, + serviceConfig, + ); err != nil { return nil, fmt.Errorf("packaging for %s: %w", serviceConfig.Name, err) } diff --git a/cli/azd/pkg/project/framework_service_python.go b/cli/azd/pkg/project/framework_service_python.go index e6bee2de71f..de2d478a576 100644 --- a/cli/azd/pkg/project/framework_service_python.go +++ b/cli/azd/pkg/project/framework_service_python.go @@ -131,10 +131,10 @@ func (pp *pythonProject) Package( packageSource, packageDest, buildForZipOptions{ - excludeCallback: func(src string) ([]excludeDirEntryCondition, error) { - return []excludeDirEntryCondition{excludeVirtualEnv, excludePyCache}, nil - }, - }); err != nil { + excludeConditions: []excludeDirEntryCondition{excludeVirtualEnv, excludePyCache}, + }, + serviceConfig, + ); err != nil { return nil, fmt.Errorf("packaging for %s: %w", serviceConfig.Name, err) } diff --git a/cli/azd/pkg/project/project_utils.go b/cli/azd/pkg/project/project_utils.go index 79b2c04a4c6..acaa8fa2e18 100644 --- a/cli/azd/pkg/project/project_utils.go +++ b/cli/azd/pkg/project/project_utils.go @@ -14,38 +14,30 @@ import ( "github.com/otiai10/copy" ) -// CreateDeployableZip creates a zip file of a folder, recursively. -// Returns the path to the created zip file or an error if it fails. +// createDeployableZip creates a zip file of a folder. func createDeployableZip(projectName string, appName string, path string) (string, error) { - // TODO: should probably avoid picking up files that weren't meant to be deployed (ie, local .env files, etc..) + // Create the output zip file path filePath := filepath.Join(os.TempDir(), fmt.Sprintf("%s-%s-azddeploy-%d.zip", projectName, appName, time.Now().Unix())) zipFile, err := os.Create(filePath) if err != nil { - return "", fmt.Errorf("failed when creating zip package to deploy %s: %w", appName, err) + return "", fmt.Errorf("failed to create zip file: %w", err) } - // Read and honor the .dotignore files - ignoreMatchers, err := dotignore.ReadIgnoreFiles(path) - if err != nil && !os.IsNotExist(err) { - return "", fmt.Errorf("reading .dotignore files: %w", err) - } - - // Create the zip file, excluding files that match the .dotignore rules - err = rzip.CreateFromDirectoryWithIgnore(path, zipFile, ignoreMatchers) + // Zip the directory without any exclusions (they've already been handled in buildForZip) + err = rzip.CreateFromDirectory(path, zipFile) if err != nil { - // If we fail here, just do our best to close things out and cleanup zipFile.Close() os.Remove(zipFile.Name()) return "", err } + // Close the zip file and return the path if err := zipFile.Close(); err != nil { - // May fail, but again, we'll do our best to cleanup here. os.Remove(zipFile.Name()) return "", err } - return zipFile.Name(), nil + return filePath, nil } // excludeDirEntryCondition resolves when a file or directory should be considered or not as part of build, when build is a @@ -55,60 +47,45 @@ type excludeDirEntryCondition func(path string, file os.FileInfo) bool // buildForZipOptions provides a set of options for doing build for zip type buildForZipOptions struct { excludeConditions []excludeDirEntryCondition - excludeCallback func(src string) ([]excludeDirEntryCondition, error) } -// buildForZip is used by projects whose build strategy is to only copy the source code into a folder, which is later -// zipped for packaging. buildForZipOptions provides the specific details for each language regarding which files should -// not be copied. -func buildForZip(src, dst string, options buildForZipOptions) error { - // Add a global exclude condition for the .zipignore file - ignoreMatchers, err := dotignore.ReadIgnoreFiles(src, ".zipignore") +// buildForZip is used by projects to prepare a directory for +// zipping, excluding files based on the ignore file and other conditions. +func buildForZip(src, dst string, options buildForZipOptions, serviceConfig *ServiceConfig) error { + // Lookup the appropriate ignore file name based on the service kind (Host) + ignoreFileName := GetIgnoreFileNameByKind(serviceConfig.Host) + + // Read and honor the specified ignore file if it exists + ignoreMatcher, err := dotignore.ReadDotIgnoreFile(src, ignoreFileName) if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("reading .zipignore files: %w", err) + return fmt.Errorf("reading %s file: %w", ignoreFileName, err) } - // Determine if the .zipignore file exists at the root of the service - zipIgnoreExists := len(ignoreMatchers) > 0 - - // Conditionally exclude virtual environments, __pycache__, and node_modules only if .zipignore doesn't exist - if !zipIgnoreExists { - if options.excludeCallback != nil { - callbackExcludes, err := options.excludeCallback(src) - if err != nil { - return fmt.Errorf("applying exclude callback: %w", err) - } - options.excludeConditions = append(options.excludeConditions, callbackExcludes...) - } - } + // Temporary array to build exclude conditions dynamically + tempExcludeConditions := []excludeDirEntryCondition{} - options.excludeConditions = append(options.excludeConditions, func(path string, file os.FileInfo) bool { - // Check if the relative path should be ignored based on .zipignore rules - if len(ignoreMatchers) > 0 { + // If there's no .ignore file, add the provided excludeConditions + if ignoreMatcher == nil { + tempExcludeConditions = append(tempExcludeConditions, options.excludeConditions...) + } else { + // If there's a .ignore file, apply ignoreMatcher only + tempExcludeConditions = append(tempExcludeConditions, func(path string, file os.FileInfo) bool { relativePath, err := filepath.Rel(src, path) - if err != nil { - return false - } - isDir := file.IsDir() - if dotignore.ShouldIgnore(relativePath, isDir, ignoreMatchers) { + if err == nil && dotignore.ShouldIgnore(relativePath, file.IsDir(), ignoreMatcher) { return true } - } - - // Always exclude .zipignore files - if filepath.Base(path) == ".zipignore" { - return true - } - - return false - }) + return false + }) + } - // These exclude conditions apply to all projects - options.excludeConditions = append(options.excludeConditions, globalExcludeAzdFolder) + // Always append the global exclusions (e.g., .azure folder) + tempExcludeConditions = append(tempExcludeConditions, globalExcludeAzdFolder) + // Copy the source directory to the destination, applying the final exclude conditions return copy.Copy(src, dst, copy.Options{ Skip: func(srcInfo os.FileInfo, src, dest string) (bool, error) { - for _, checkExclude := range options.excludeConditions { + // Apply exclude conditions (either the default or the ignoreMatcher) + for _, checkExclude := range tempExcludeConditions { if checkExclude(src, srcInfo) { return true, nil } diff --git a/cli/azd/pkg/project/service_target.go b/cli/azd/pkg/project/service_target.go index fea7b41e0e2..a21c799b833 100644 --- a/cli/azd/pkg/project/service_target.go +++ b/cli/azd/pkg/project/service_target.go @@ -94,6 +94,19 @@ type ServiceTarget interface { ) ([]string, error) } +// GetIgnoreFileNameByKind returns the appropriate ignore file name (e.g., .funcignore, .webappignore) +// based on the service target kind. +func GetIgnoreFileNameByKind(kind ServiceTargetKind) string { + switch kind { + case AzureFunctionTarget: + return ".funcignore" + case AppServiceTarget: + return ".webappignore" + default: + return "" + } +} + // NewServiceDeployResult is a helper function to create a new ServiceDeployResult func NewServiceDeployResult( relatedResourceId string, diff --git a/cli/azd/pkg/rzip/rzip.go b/cli/azd/pkg/rzip/rzip.go index 84bd8b74ac5..db7b0409421 100644 --- a/cli/azd/pkg/rzip/rzip.go +++ b/cli/azd/pkg/rzip/rzip.go @@ -10,8 +10,6 @@ import ( "os" "path/filepath" "strings" - - "github.com/denormal/go-gitignore" ) func CreateFromDirectory(source string, buf *os.File) error { @@ -21,104 +19,55 @@ func CreateFromDirectory(source string, buf *os.File) error { return err } - if info.IsDir() { - return nil - } - fileInfo, err := info.Info() - if err != nil { - return err - } - - header := &zip.FileHeader{ - Name: strings.Replace( - strings.TrimPrefix( - strings.TrimPrefix(path, source), - string(filepath.Separator)), "\\", "/", -1), - Modified: fileInfo.ModTime(), - Method: zip.Deflate, - } - - f, err := w.CreateHeader(header) - if err != nil { - return err - } - in, err := os.Open(path) - if err != nil { - return err - } - _, err = io.Copy(f, in) + // Use os.Lstat to get file info without following symlinks + fileInfo, err := os.Lstat(path) if err != nil { return err } - return nil - }) - if err != nil { - return err - } - return w.Close() -} - -// CreateFromDirectoryWithIgnore creates a zip archive from the contents of a directory, excluding files -// that match any of the provided ignore rules. -func CreateFromDirectoryWithIgnore(srcDir string, writer io.Writer, ignoreMatchers []gitignore.GitIgnore) error { - zipWriter := zip.NewWriter(writer) - defer zipWriter.Close() - - err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // Get the relative path of the file to ensure the root directory isn't included in the zip file - relativePath, err := filepath.Rel(srcDir, path) - if err != nil { - return err - } - - // Skip the root directory itself - if relativePath == "." { + // Skip symbolic links + if fileInfo.Mode()&os.ModeSymlink != 0 { return nil } - // Check if the file should be ignored based on the ignore matchers - isDir := info.IsDir() - if shouldIgnore(relativePath, isDir, ignoreMatchers) { - if isDir { - // If a directory should be ignored, skip its contents as well - return filepath.SkipDir - } - // Otherwise, just skip the file - return nil + // Add directories to the zip archive + relativePath := strings.Replace( + strings.TrimPrefix( + strings.TrimPrefix(path, source), + string(filepath.Separator)), "\\", "/", -1) + + if fileInfo.IsDir() { + // Add trailing slash for directories in zip + relativePath += "/" } - // Add the file or directory to the zip archive - header, err := zip.FileInfoHeader(info) + // Create a zip header based on file info + header, err := zip.FileInfoHeader(fileInfo) if err != nil { return err } header.Name = relativePath - // Ensure directories are properly handled - if isDir { - header.Name += "/" - } else { + // Add files with compression + if !fileInfo.IsDir() { header.Method = zip.Deflate } - writer, err := zipWriter.CreateHeader(header) + // Create the header in the zip file + writer, err := w.CreateHeader(header) if err != nil { return err } - if !isDir { - file, err := os.Open(path) + // Only copy contents for files (not directories) + if !fileInfo.IsDir() { + in, err := os.Open(path) if err != nil { return err } - defer file.Close() + defer in.Close() - _, err = io.Copy(writer, file) + _, err = io.Copy(writer, in) if err != nil { return err } @@ -126,16 +75,9 @@ func CreateFromDirectoryWithIgnore(srcDir string, writer io.Writer, ignoreMatche return nil }) - - return err -} - -// shouldIgnore determines whether a file or directory should be ignored based on the provided ignore matchers. -func shouldIgnore(relativePath string, isDir bool, ignoreMatchers []gitignore.GitIgnore) bool { - for _, matcher := range ignoreMatchers { - if match := matcher.Relative(relativePath, isDir); match != nil && match.Ignore() { - return true - } + if err != nil { + return err } - return false + + return w.Close() } diff --git a/cli/azd/test/functional/package_test.go b/cli/azd/test/functional/package_test.go index 5b73e083e58..d217dded3f8 100644 --- a/cli/azd/test/functional/package_test.go +++ b/cli/azd/test/functional/package_test.go @@ -163,6 +163,46 @@ func Test_CLI_Package(t *testing.T) { require.Contains(t, packageResult.Stdout, fmt.Sprintf("Package Output: %s", os.TempDir())) } +/* +Test_CLI_Package_ZipIgnore + +This test verifies that the packaging logic correctly handles the inclusion and exclusion of files +based on the presence or absence of `.webappignore` and `.funcignore` files. The following scenarios are covered: + +1. Node_App_Service_With_Webappignore + - Verifies that `node_modules` is included when `.webappignore` is present, + and that specific files like `logs/log.txt` are excluded as per the rules defined in the `.webappignore` file. + +2. Python_App_Service_With_Webappignore + - Verifies that Python-specific files like `__pycache__` and `.venv` are included when `.webappignore` is present, + and files like `logs/log.txt` are excluded based on the rules in the `.webappignore`. + +3. Python_App_Service_With_Pycache_Excluded + - Verifies that `__pycache__` is excluded when a `.webappignore` file explicitly contains a rule to exclude it, + while other directories like `.venv` are included since there is no exclusion rule for them. + +4. Function_App_With_Funcignore + - Verifies that a Function App respects the `.funcignore` file, ensuring that `logs/log.txt` is excluded + as per the rules defined in `.funcignore`. + +5. Node_App_Service_Without_Webappignore + - Verifies that `node_modules` is excluded when no `.webappignore` is present, + and that files like `logs/log.txt` are included since no exclusion rules apply without the `.webappignore`. + +6. Python_App_Service_Without_Webappignore + - Verifies that Python-specific files like `__pycache__` + and `.venv` are excluded by default when no `.webappignore` is present, + and that files like `logs/log.txt` are included. + +7. Function_App_Without_Funcignore + - Verifies that when no `.funcignore` file is present, no exclusions are applied, and files such as `logs/log.txt` + are included in the package. + +For each scenario, the test simulates the presence or absence of the relevant + `.ignore` files and checks the contents of the resulting + zip package to ensure the correct files are included or excluded as expected. +*/ + func Test_CLI_Package_ZipIgnore(t *testing.T) { t.Parallel() ctx, cancel := newTestContext(t) @@ -185,8 +225,10 @@ func Test_CLI_Package_ZipIgnore(t *testing.T) { // Print directory contents for debugging if printDebug is true if printDebug { - printDirContents(t, "service1", filepath.Join(dir, "src", "service1")) - printDirContents(t, "service2", filepath.Join(dir, "src", "service2")) + printDirContents(t, "service_node", filepath.Join(dir, "src", "service_node")) + printDirContents(t, "service_python", filepath.Join(dir, "src", "service_python")) + printDirContents(t, "service_python_pycache", filepath.Join(dir, "src", "service_python_pycache")) + printDirContents(t, "service_function", filepath.Join(dir, "src", "service_function")) } // Run the init command to initialize the project @@ -202,119 +244,107 @@ func Test_CLI_Package_ZipIgnore(t *testing.T) { // Define the scenarios to test scenarios := []struct { - name string - description string - enabled bool - rootZipIgnore string - service1ZipIgnore string - expectedFiles map[string]map[string]bool + name string + description string + serviceName string // This is the actual service name used in the directory + expectedFiles map[string]bool + shouldDeleteIgnoreFile bool // Flag to simulate the absence of .webappignore or .funcignore }{ { - name: "No zipignore", - description: "Tests the default behavior when no .zipignore files are present. " + - "Verifies that common directories like __pycache__, .venv, and node_modules are excluded.", - enabled: true, - expectedFiles: map[string]map[string]bool{ - "service1": { - "testfile.py": true, - "__pycache__/testcache.txt": false, - ".venv/pyvenv.cfg": false, - "logs/log.txt": true, - }, - "service2": { - "testfile.js": true, - "node_modules/some_package/package.json": false, - "logs/log.txt": true, - }, + name: "Node_App_Service_With_Webappignore", + description: "Verifies that node_modules are included when " + + ".webappignore is present, and logs/log.txt is excluded.", + serviceName: "service_node", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.js": true, + "node_modules/some_package/index.js": true, // Included because .webappignore is present + "logs/log.txt": false, // Excluded by .webappignore }, }, { - name: "Root zipignore excluding pycache", - description: "Tests the behavior when a root .zipignore excludes __pycache__. " + - "Verifies that __pycache__ is excluded in both services, but other directories are included.", - enabled: true, - rootZipIgnore: "__pycache__\n", - expectedFiles: map[string]map[string]bool{ - "service1": { - "testfile.py": true, - "__pycache__/testcache.txt": false, - ".venv/pyvenv.cfg": true, - "logs/log.txt": true, - }, - "service2": { - "testfile.js": true, - "node_modules/some_package/package.json": true, - "logs/log.txt": true, - }, + name: "Python_App_Service_With_Webappignore", + description: "Verifies that __pycache__ and .venv are included when " + + " .webappignore is present, and logs/log.txt is excluded.", + serviceName: "service_python", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": true, // Included because .webappignore is present + ".venv/pyvenv.cfg": true, // Included because .webappignore is present + "logs/log.txt": false, // Excluded by .webappignore }, }, { - name: "Root and Service1 zipignore", - description: "Tests the behavior when both the root and Service1 have .zipignore files. " + - "Verifies that the root .zipignore affects both services, but Service1's .zipignore " + - "takes precedence for its own files.", - enabled: true, - rootZipIgnore: "logs/\n", - service1ZipIgnore: "__pycache__\n", - expectedFiles: map[string]map[string]bool{ - "service1": { - "testfile.py": true, - "__pycache__/testcache.txt": false, - ".venv/pyvenv.cfg": true, - "logs/log.txt": false, - }, - "service2": { - "testfile.js": true, - "node_modules/some_package/package.json": true, - "logs/log.txt": false, - }, + name: "Python_App_Service_With_Pycache_Excluded", + description: "Verifies that __pycache__ is excluded when .webappignore has a rule to exclude it.", + serviceName: "service_python_pycache", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": false, // Excluded by .webappignore rule + ".venv/pyvenv.cfg": true, // Included because no exclusion rule + "logs/log.txt": false, // Excluded by .webappignore }, }, { - name: "Service1 zipignore only", - description: "Tests the behavior when only Service1 has a .zipignore file. " + - "Verifies that Service1 follows its .zipignore, while Service2 uses the default behavior.", - enabled: true, - service1ZipIgnore: "__pycache__\n", - expectedFiles: map[string]map[string]bool{ - "service1": { - "testfile.py": true, - "__pycache__/testcache.txt": false, - ".venv/pyvenv.cfg": true, - "logs/log.txt": true, - }, - "service2": { - "testfile.js": true, - "node_modules/some_package/package.json": false, - "logs/log.txt": true, - }, + name: "Function_App_With_Funcignore", + description: "Verifies that logs/log.txt is excluded when .funcignore is present.", + serviceName: "service_function", + shouldDeleteIgnoreFile: false, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": true, + ".venv/pyvenv.cfg": true, + "logs/log.txt": false, // Excluded by .funcignore + }, + }, + { + name: "Node_App_Service_Without_Webappignore", + description: "Verifies that node_modules is excluded when .webappignore is not present.", + serviceName: "service_node", + shouldDeleteIgnoreFile: true, + expectedFiles: map[string]bool{ + "testfile.js": true, + "node_modules/some_package/index.js": false, // Excluded because no .webappignore + "logs/log.txt": true, // Included because no .webappignore + }, + }, + { + name: "Python_App_Service_Without_Webappignore", + description: "Verifies that __pycache__ and .venv are excluded when .webappignore is not present.", + serviceName: "service_python", + shouldDeleteIgnoreFile: true, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": false, // Excluded because no .webappignore + ".venv/pyvenv.cfg": false, // Excluded because no .webappignore + "logs/log.txt": true, // Included because no .webappignore + }, + }, + { + name: "Function_App_Without_Funcignore", + description: "Verifies that logs/log.txt is included when .funcignore is not present.", + serviceName: "service_function", + shouldDeleteIgnoreFile: true, + expectedFiles: map[string]bool{ + "testfile.py": true, + "__pycache__/testcache.txt": false, + ".venv/pyvenv.cfg": false, + "logs/log.txt": true, // Included because no .funcignore }, }, } for _, scenario := range scenarios { - if !scenario.enabled { - continue - } - t.Run(scenario.name, func(t *testing.T) { // Print the scenario description t.Logf("Scenario: %s - %s", scenario.name, scenario.description) - // Set up .zipignore files based on the scenario - if scenario.rootZipIgnore != "" { - err := os.WriteFile(filepath.Join(dir, ".zipignore"), []byte(scenario.rootZipIgnore), 0600) - require.NoError(t, err) - } - if scenario.service1ZipIgnore != "" { - err := os.WriteFile(filepath.Join(dir, "src", "service1", ".zipignore"), - []byte(scenario.service1ZipIgnore), 0600) - require.NoError(t, err) - } - - // Print directory contents after writing .zipignore if printDebug is true - if printDebug { - printDirContents(t, "service1", filepath.Join(dir, "src", "service1")) + // If we're simulating the absence of the ignore file, delete it + if scenario.shouldDeleteIgnoreFile { + os.Remove(filepath.Join(dir, "src", scenario.serviceName, ".webappignore")) + os.Remove(filepath.Join(dir, "src", scenario.serviceName, ".funcignore")) } // Run the package command and specify an output path @@ -325,26 +355,11 @@ func Test_CLI_Package_ZipIgnore(t *testing.T) { _, err = cli.RunCommand(ctx, "package", "--output-path", outputDir) require.NoError(t, err) - // Print directory contents of the output directory if printDebug is true - if printDebug { - printDirContents(t, scenario.name+" output", outputDir) - } - - // Verify that the package was created and the output directory exists - files, err := os.ReadDir(outputDir) - require.NoError(t, err) - require.Len(t, files, 2) - - // Check contents of Service1 package - checkServicePackage(t, outputDir, "service1", scenario.expectedFiles["service1"], printDebug) - - // Check contents of Service2 package - checkServicePackage(t, outputDir, "service2", scenario.expectedFiles["service2"], printDebug) + // Check contents of Service package + checkServicePackage(t, outputDir, scenario.serviceName, scenario.expectedFiles, printDebug) - // Clean up .zipignore files and generated zip files + // Clean up generated zip files and ignore files os.RemoveAll(outputDir) - os.Remove(filepath.Join(dir, ".zipignore")) - os.Remove(filepath.Join(dir, "src", "service1", ".zipignore")) }) } } diff --git a/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml b/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml index 0f9109900fa..4461b426be1 100644 --- a/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml +++ b/cli/azd/test/functional/testdata/samples/dotignore/azure.yaml @@ -2,11 +2,19 @@ name: dotignoretest services: - service1: - project: src/service1 + service_python: + project: src/service_python language: python host: appservice - service2: - project: src/service2 + service_node: + project: src/service_node language: js + host: appservice + service_function: + project: src/service_function + language: python + host: function + service_python_pycache: + project: src/service_python_pycache + language: python host: appservice \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.funcignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.funcignore new file mode 100644 index 00000000000..6d2b2ed9cc7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.funcignore @@ -0,0 +1 @@ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service1/.venv/pyvenv.cfg b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.venv/pyvenv.cfg similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service1/.venv/pyvenv.cfg rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_function/.venv/pyvenv.cfg diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service1/__pycache__/testcache.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/__pycache__/testcache.txt similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service1/__pycache__/testcache.txt rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_function/__pycache__/testcache.txt diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service1/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/logs/log.txt similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service1/logs/log.txt rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_function/logs/log.txt diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service1/testfile.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_function/testfile.py similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service1/testfile.py rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_function/testfile.py diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/.webappignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/.webappignore new file mode 100644 index 00000000000..6d2b2ed9cc7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/.webappignore @@ -0,0 +1 @@ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/logs/log.txt similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/logs/log.txt rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/logs/log.txt diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/node_modules/.package-lock.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/.package-lock.json similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/node_modules/.package-lock.json rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/.package-lock.json diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/node_modules/some_package/index.js b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/index.js similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/node_modules/some_package/index.js rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/index.js diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/node_modules/some_package/package.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/package.json similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/node_modules/some_package/package.json rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/node_modules/some_package/package.json diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/package-lock.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package-lock.json similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/package-lock.json rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package-lock.json diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/package.json b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package.json similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/package.json rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/package.json diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service2/testfile.js b/cli/azd/test/functional/testdata/samples/dotignore/src/service_node/testfile.js similarity index 100% rename from cli/azd/test/functional/testdata/samples/dotignore/src/service2/testfile.js rename to cli/azd/test/functional/testdata/samples/dotignore/src/service_node/testfile.js diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/lib64/python3.10/site-packages/_yaml/__init__.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/lib64/python3.10/site-packages/_yaml/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/pyvenv.cfg b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/pyvenv.cfg new file mode 100644 index 00000000000..2bc7ab42877 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.venv/pyvenv.cfg @@ -0,0 +1 @@ +home = /usr/local/bin/python diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.webappignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.webappignore new file mode 100644 index 00000000000..6d2b2ed9cc7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/.webappignore @@ -0,0 +1 @@ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/__pycache__/testcache.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/__pycache__/testcache.txt new file mode 100644 index 00000000000..d24b1df1aca --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/__pycache__/testcache.txt @@ -0,0 +1 @@ +binary data diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/logs/log.txt new file mode 100644 index 00000000000..c3463ec70c8 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/logs/log.txt @@ -0,0 +1 @@ +Log data... diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/testfile.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/testfile.py new file mode 100644 index 00000000000..7df869a15e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python/testfile.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.venv/pyvenv.cfg b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.venv/pyvenv.cfg new file mode 100644 index 00000000000..2bc7ab42877 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.venv/pyvenv.cfg @@ -0,0 +1 @@ +home = /usr/local/bin/python diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.webappignore b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.webappignore new file mode 100644 index 00000000000..bb379adcb0b --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/.webappignore @@ -0,0 +1,2 @@ +__pycache__/ +logs/log.txt \ No newline at end of file diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/__pycache__/testcache.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/__pycache__/testcache.txt new file mode 100644 index 00000000000..d24b1df1aca --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/__pycache__/testcache.txt @@ -0,0 +1 @@ +binary data diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/logs/log.txt b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/logs/log.txt new file mode 100644 index 00000000000..c3463ec70c8 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/logs/log.txt @@ -0,0 +1 @@ +Log data... diff --git a/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/testfile.py b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/testfile.py new file mode 100644 index 00000000000..7df869a15e7 --- /dev/null +++ b/cli/azd/test/functional/testdata/samples/dotignore/src/service_python_pycache/testfile.py @@ -0,0 +1 @@ +print("Hello, World!")