Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 3 additions & 18 deletions pkg/buildpacks/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"runtime"
Expand Down Expand Up @@ -142,23 +141,9 @@ func (b *Builder) Build(ctx context.Context, f fn.Function, platforms []fn.Platf
buildpacks = defaultBuildpacks[f.Runtime]
}

// Reading .funcignore file
var excludes []string
filePath := filepath.Join(f.Root, ".funcignore")
file, err := os.Open(filePath)
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("\nfailed to open file: %s", err)
}
} else {
defer file.Close()
buf := new(bytes.Buffer)
_, err := io.Copy(buf, file)
if err != nil {
return fmt.Errorf("\nfailed to read file: %s", err)
}
excludes = strings.Split(buf.String(), "\n")
}
// Reading .funcignore file (user patterns only — pack handles its own
// build context and needs access to directories like .func/build)
excludes := fn.ParseFuncIgnore(f.Root)
// Pack build options
opts := pack.BuildOptions{
GroupID: -1,
Expand Down
46 changes: 23 additions & 23 deletions pkg/buildpacks/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,45 +145,45 @@ func TestBuild_BuilderImageConfigurable(t *testing.T) {
}
}

// TestBuild_BuilderImageExclude ensures that ignored files are not added to the func
// image
func TestBuild_BuilderImageExclude(t *testing.T) {
// TestBuild_BuilderImageExcludePatterns verifies that all supported
// .funcignore pattern forms are correctly passed to pack's Exclude option.
func TestBuild_BuilderImageExcludePatterns(t *testing.T) {
root, done := Mktemp(t)
defer done()

var (
i = &mockImpl{} // mock underlying implementation
b = NewBuilder( // Func Builder logic
WithName(builders.Pack), WithImpl(i))
f = fn.Function{
Runtime: "go",
Root: root,
Registry: "example.com/alice",
}
i = &mockImpl{}
b = NewBuilder(WithName(builders.Pack), WithImpl(i))
f = fn.Function{Runtime: "go", Root: root, Registry: "example.com/alice"}
err error
)

// Initialize the function to create proper source files
if f, err = fn.New().Init(f); err != nil {
t.Fatal(err)
}

funcIgnoreContent := []byte(`#testing comments
hello.txt`)
expected := []string{"hello.txt"}

//create a .funcignore file containing the details of the files to be ignored
err = os.WriteFile(filepath.Join(f.Root, ".funcignore"), funcIgnoreContent, 0644)
if err != nil {
content := []byte("# comment stripped\nnotes.txt\n*.tmp\n/docs\ndist/\n")
if err = os.WriteFile(filepath.Join(f.Root, ".funcignore"), content, 0644); err != nil {
t.Fatal(err)
}

i.BuildFn = func(ctx context.Context, opts pack.BuildOptions) error {
if len(opts.ProjectDescriptor.Build.Exclude) != 2 {
t.Fatalf("expected 2 lines of exclusions , got %v", len(opts.ProjectDescriptor.Build.Exclude))
excludes := opts.ProjectDescriptor.Build.Exclude
// 4 user patterns: notes.txt, *.tmp, /docs, dist/ (comment stripped)
if len(excludes) != 4 {
t.Fatalf("expected 4 exclusions, got %v: %v", len(excludes), excludes)
}
want := map[string]bool{"notes.txt": true, "*.tmp": true, "/docs": true, "dist/": true}
for _, e := range excludes {
if !want[e] {
t.Errorf("unexpected exclusion: %q", e)
}
}
if opts.ProjectDescriptor.Build.Exclude[1] != expected[0] {
t.Fatalf("expected excluded file to be '%v', got '%v'", expected[1], opts.ProjectDescriptor.Build.Exclude[1])
// Verify comment was stripped
for _, e := range excludes {
if len(e) > 0 && e[0] == '#' {
t.Errorf("comment line in excludes: %q", e)
}
}
return nil
}
Expand Down
102 changes: 91 additions & 11 deletions pkg/functions/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1334,28 +1334,102 @@ func ensureFuncIgnore(root string) error {
defer file.Close()

// Write the desired string to the file
_, err = file.WriteString(`
# Use the .funcignore file to exclude files which should not be
# tracked in the image build. To instruct the system not to track
# files in the image build, add the regex pattern or file information
# to this file.
_, err = file.WriteString(`# Use the .funcignore file to exclude files from the image build.
# Excluded files do not affect the rebuild fingerprint.
#
# Supported patterns:
# foo - exclude any file or directory named "foo" at any depth
# *.tmp - glob; exclude matching files at any depth (except S2I: root only)
# /docs - root-relative; exclude only the top-level "docs" entry
# build/ - trailing slash stripped, then matched as a basename
#
# Lines starting with # are comments and are ignored.
`)
if err != nil {
return err
}
return nil
}

// DefaultIgnored contains entries that are always ignored regardless of
// .funcignore file contents.
var DefaultIgnored = []string{
".git",
".func",
".funcignore",
".gitignore",
}

// ParseFuncIgnore reads .funcignore and returns patterns with comments
// and blank lines stripped.
func ParseFuncIgnore(root string) []string {
f, err := os.Open(filepath.Join(root, ".funcignore"))
if err != nil {
return nil
}
defer f.Close()

var patterns []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
patterns = append(patterns, line)
}
return patterns
}

// IsIgnored reports whether a path should be skipped based on the
// default ignored entries and user-defined patterns from .funcignore.
// Four pattern forms are supported:
//
// "/foo" — root-relative; matches that exact path and everything beneath it.
// "foo/" — trailing slash stripped, then matched as basename.
// "*.tmp" — glob; matched against basename and full relative path (filepath.Match semantics).
// "foo" — basename; matches any entry named "foo" at any depth.
//
// Used by Fingerprint and the OCI builder. Known differences from other builders:
// - Pack (go-gitignore): supports "**" recursive globs and "!" negation — this does not.
// - S2I (filepath.Glob): non-recursive — "*.tmp" only matches at root level, this matches at any depth.
func IsIgnored(relPath string, userPatterns []string) bool {
base := filepath.Base(relPath)
for _, d := range DefaultIgnored {
if base == d {
return true
}
}
slashRel := filepath.ToSlash(relPath)
for _, pattern := range userPatterns {
if strings.HasPrefix(pattern, "/") {
p := pattern[1:]
if slashRel == p || strings.HasPrefix(slashRel, p+"/") {
return true
}
continue
}
pattern = strings.TrimSuffix(pattern, "/")
if matched, _ := filepath.Match(pattern, base); matched {
return true
}
if matched, _ := filepath.Match(pattern, relPath); matched {
return true
}
}
return false
}

// Fingerprint the files at a given path. Returns a hash calculated from the
// filenames and modification timestamps of the files within the given root.
// Also returns a logfile consisting of the filenames and modification times
// which contributed to the hash.
// Intended to determine if there were appreciable changes to a function's
// source code, certain directories and files are ignored, such as
// .git and .func.
// Future updates will include files explicitly marked as ignored by a
// .funcignore.
// .git and .func. User-defined patterns from .funcignore are also respected.
func Fingerprint(root string) (hash, log string, err error) {
userPatterns := ParseFuncIgnore(root)

h := sha256.New() // Hash builder
l := bytes.Buffer{} // Log buffer

Expand All @@ -1366,9 +1440,15 @@ func Fingerprint(root string) (hash, log string, err error) {
if path == root {
return nil
}
// Always ignore .func, .git (TODO: .funcignore)
if info.IsDir() && (info.Name() == RunDataDir || info.Name() == ".git") {
return filepath.SkipDir
relPath, err := filepath.Rel(root, path)
if err != nil {
return err
}
if IsIgnored(relPath, userPatterns) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
fmt.Fprintf(h, "%v:%v:", path, info.ModTime().UnixNano()) // Write to the Hasher
fmt.Fprintf(&l, "%v:%v\n", path, info.ModTime().UnixNano()) // Write to the Log
Expand Down
Loading
Loading