Skip to content

Commit

Permalink
Add support for azdignore in package
Browse files Browse the repository at this point in the history
  • Loading branch information
jongio committed Aug 23, 2024
1 parent 2f8384a commit aef9154
Show file tree
Hide file tree
Showing 15 changed files with 383 additions and 134 deletions.
10 changes: 7 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"ghcr.io/devcontainers/features/go:1": {
"version": "1.21"
},
"ghcr.io/guiyomh/features/golangci-lint:0":{},
"ghcr.io/guiyomh/features/golangci-lint:0": {},
"ghcr.io/devcontainers/features/docker-in-docker:2.11.0": {
"version": "latest",
"moby": true
Expand All @@ -19,7 +19,6 @@
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/terraform:1": {}

},
"customizations": {
"vscode": {
Expand All @@ -34,5 +33,10 @@
]
}
},
"postCreateCommand": "go install gotest.tools/gotestsum@latest"
"postCreateCommand": "go install gotest.tools/gotestsum@latest",
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined"
]
}
1 change: 1 addition & 0 deletions .vscode/cspell.global.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ ignoreWords:
- fdfp
- fics
- Frontdoor
- gitcli
- golobby
- graphsdk
- hndl
Expand Down
92 changes: 3 additions & 89 deletions cli/azd/internal/repository/initializer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"path/filepath"
"strings"

"github.com/azure/azure-dev/cli/azd/pkg/azdignore"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/input"
Expand All @@ -24,7 +25,6 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/tools/dotnet"
"github.com/azure/azure-dev/cli/azd/pkg/tools/git"
"github.com/azure/azure-dev/cli/azd/resources"
"github.com/denormal/go-gitignore"
"github.com/otiai10/copy"
)

Expand Down Expand Up @@ -86,12 +86,12 @@ func (i *Initializer) Initialize(
return err
}

ignoreMatcher, err := readIgnoreFile(staging)
ignoreMatcher, err := azdignore.ReadIgnoreFiles(staging)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("reading .azdignore file: %w", err)
}

err = removeIgnoredFiles(staging, ignoreMatcher)
err = azdignore.RemoveIgnoredFiles(staging, ignoreMatcher)
if err != nil {
return fmt.Errorf("removing ignored files: %w", err)
}
Expand Down Expand Up @@ -139,92 +139,6 @@ func (i *Initializer) Initialize(
return nil
}

// readIgnoreFile reads the .azdignore file and returns a gitignore.GitIgnore structure
func readIgnoreFile(projectDir string) (gitignore.GitIgnore, error) {
ignoreFilePath := filepath.Join(projectDir, ".azdignore")
if _, err := os.Stat(ignoreFilePath); os.IsNotExist(err) {
return nil, nil // No .azdignore file, no need to ignore any files
}

ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath)
if err != nil {
return nil, fmt.Errorf("error reading .azdignore file: %w", err)
}

return ignoreMatcher, 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
}

// removeIgnoredFiles removes files and directories based on .azdignore rules using a pre-collected list of paths
func removeIgnoredFiles(staging string, ignoreMatcher gitignore.GitIgnore) error {
if ignoreMatcher == nil {
return nil // No .azdignore file, 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()
}

match := ignoreMatcher.Relative(relativePath, isDir)
if match != nil && match.Ignore() {
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
}

func (i *Initializer) fetchCode(
ctx context.Context,
templateUrl string,
Expand Down
120 changes: 120 additions & 0 deletions cli/azd/pkg/azdignore/azdignore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package azdignore

import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/denormal/go-gitignore"
)

// ReadIgnoreFiles reads all .azdignore files in the directory hierarchy, from the projectDir upwards,
// and returns a slice of gitignore.GitIgnore structures.
func ReadIgnoreFiles(projectDir string) ([]gitignore.GitIgnore, error) {
var ignoreMatchers []gitignore.GitIgnore

// Traverse upwards from the projectDir to the root directory
currentDir := projectDir
for {
ignoreFilePath := filepath.Join(currentDir, ".azdignore")
if _, err := os.Stat(ignoreFilePath); !os.IsNotExist(err) {
ignoreMatcher, err := gitignore.NewFromFile(ignoreFilePath)
if err != nil {
return nil, fmt.Errorf("error reading .azdignore file at %s: %w", ignoreFilePath, err)
}
ignoreMatchers = append([]gitignore.GitIgnore{ignoreMatcher}, ignoreMatchers...)
}

// Stop if we've reached the root directory
parentDir := filepath.Dir(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir
}

return ignoreMatchers, 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() {
return true
}
}
return false
}

// RemoveIgnoredFiles removes files and directories based on .azdignore rules using a pre-collected list of paths.
func RemoveIgnoredFiles(staging string, ignoreMatchers []gitignore.GitIgnore) error {
if len(ignoreMatchers) == 0 {
return nil // No .azdignore 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
}
51 changes: 44 additions & 7 deletions cli/azd/pkg/project/project_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"time"

"github.com/azure/azure-dev/cli/azd/pkg/azdignore"
"github.com/azure/azure-dev/cli/azd/pkg/rzip"
"github.com/otiai10/copy"
)
Expand All @@ -23,15 +24,23 @@ func createDeployableZip(projectName string, appName string, path string) (strin
return "", fmt.Errorf("failed when creating zip package to deploy %s: %w", appName, err)
}

if err := rzip.CreateFromDirectory(path, zipFile); err != nil {
// if we fail here just do our best to close things out and cleanup
// Read and honor the .azdignore files
ignoreMatchers, err := azdignore.ReadIgnoreFiles(path)
if err != nil && !os.IsNotExist(err) {
return "", fmt.Errorf("reading .azdignore files: %w", err)
}

// Create the zip file, excluding files that match the .azdignore rules
err = rzip.CreateFromDirectoryWithIgnore(path, zipFile, ignoreMatchers)
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
}

if err := zipFile.Close(); err != nil {
// may fail but, again, we'll do our best to cleanup here.
// May fail, but again, we'll do our best to cleanup here.
os.Remove(zipFile.Name())
return "", err
}
Expand All @@ -48,12 +57,40 @@ type buildForZipOptions struct {
excludeConditions []excludeDirEntryCondition
}

// buildForZip is use by projects which build strategy is to only copy the source code into a folder which is later
// zipped for packaging. For example Python and Node framework languages. buildForZipOptions provides the specific
// details for each language which should not be ever copied.
// 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 .azdignore file
ignoreMatchers, err := azdignore.ReadIgnoreFiles(src)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("reading .azdignore files: %w", err)
}

options.excludeConditions = append(options.excludeConditions, func(path string, file os.FileInfo) bool {
if len(ignoreMatchers) > 0 {
// Get the relative path from the source directory
relativePath, err := filepath.Rel(src, path)
if err != nil {
return false
}

// Check if the relative path should be ignored
isDir := file.IsDir()
if azdignore.ShouldIgnore(relativePath, isDir, ignoreMatchers) {
return true
}
}

// Always exclude .azdignore files
if filepath.Base(path) == ".azdignore" {
return true
}

return false
})

// these exclude conditions applies to all projects
// These exclude conditions apply to all projects
options.excludeConditions = append(options.excludeConditions, globalExcludeAzdFolder)

return copy.Copy(src, dst, copy.Options{
Expand Down
Loading

0 comments on commit aef9154

Please sign in to comment.