Skip to content
Merged
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
1 change: 1 addition & 0 deletions buildkit/frontend.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// for platforms: used by `ghcr.io/railwayapp/railpack-frontend` to accept railpack plans via BuildKit
package buildkit

import (
Expand Down
24 changes: 12 additions & 12 deletions core/generate/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,16 @@ func NewGenerateContext(app *a.App, env *a.Environment, config *config.Config, l
return nil, err
}

dockerignoreCtx := plan.NewDockerignoreContext(app)
excludes, includes, err := dockerignoreCtx.ParseWithLogging(logger)
dockerignoreCtx, err := plan.NewDockerignoreContext(app)
if err != nil {
return nil, fmt.Errorf("failed to parse .dockerignore: %w", err)
}

if excludes != nil || includes != nil {
log.Debugf("Dockerignore exclude patterns: %v", excludes)
log.Debugf("Dockerignore include patterns: %v", includes)

if dockerignoreCtx.HasFile {
logger.LogInfo("Found .dockerignore file, applying filters")

log.Debugf("Dockerignore exclude patterns: %v", dockerignoreCtx.Excludes)
log.Debugf("Dockerignore include patterns: %v", dockerignoreCtx.Includes)
}

ctx := &GenerateContext{
Expand All @@ -97,7 +98,7 @@ func NewGenerateContext(app *a.App, env *a.Environment, config *config.Config, l

ctx.applyPackagesFromConfig()

if excludes != nil || includes != nil {
if dockerignoreCtx.HasFile {
ctx.Metadata.SetBool("dockerIgnore", true)
}

Expand Down Expand Up @@ -268,12 +269,11 @@ func (c *GenerateContext) applyConfig() {
func (c *GenerateContext) NewLocalLayer() plan.Layer {
layer := plan.NewLocalLayer()

excludes, includes, _ := c.dockerignoreCtx.Parse()
if len(includes) > 0 {
layer.Filter.Include = utils.RemoveDuplicates(append(layer.Filter.Include, includes...))
if len(c.dockerignoreCtx.Includes) > 0 {
layer.Filter.Include = append(layer.Filter.Include, c.dockerignoreCtx.Includes...)
}
if len(excludes) > 0 {
layer.Filter.Exclude = utils.RemoveDuplicates(append(layer.Filter.Exclude, excludes...))
if len(c.dockerignoreCtx.Excludes) > 0 {
layer.Filter.Exclude = append(layer.Filter.Exclude, c.dockerignoreCtx.Excludes...)
}

return layer
Expand Down
5 changes: 2 additions & 3 deletions core/generate/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,8 @@ func TestGenerateContextDockerignore(t *testing.T) {
require.NotNil(t, ctx.dockerignoreCtx)

// Verify parsing works with no file present
excludes, includes, _ := ctx.dockerignoreCtx.Parse()
require.Nil(t, excludes)
require.Nil(t, includes)
require.Nil(t, ctx.dockerignoreCtx.Excludes)
require.Nil(t, ctx.dockerignoreCtx.Includes)
})

t.Run("context creation fails with invalid dockerignore", func(t *testing.T) {
Expand Down
57 changes: 22 additions & 35 deletions core/plan/dockerignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package plan
import (
"strings"

"github.com/moby/patternmatcher/ignorefile"
"github.com/railwayapp/railpack/core/app"
"github.com/railwayapp/railpack/internal/utils"

// this is the native dockerignore parser used by buildkit
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/dockerignore/dockerignore_deprecated.go
"github.com/moby/patternmatcher/ignorefile"
)

// checks if a .dockerignore file exists in the app directory and parses it
Expand Down Expand Up @@ -53,43 +57,26 @@ func separatePatterns(patterns []string) (excludes []string, includes []string)
}

type DockerignoreContext struct {
parsed bool
excludes []string
includes []string
app *app.App
}

func NewDockerignoreContext(app *app.App) *DockerignoreContext {
return &DockerignoreContext{
app: app,
}
}

// Parse parses the .dockerignore file and caches the results
func (d *DockerignoreContext) Parse() ([]string, []string, error) {
if !d.parsed {
excludes, includes, err := CheckAndParseDockerignore(d.app)
if err != nil {
return nil, nil, err
}

d.excludes = excludes
d.includes = includes
d.parsed = true
}

return d.excludes, d.includes, nil
Excludes []string
Includes []string
HasFile bool
}

func (d *DockerignoreContext) ParseWithLogging(logger interface{ LogInfo(string, ...interface{}) }) ([]string, []string, error) {
excludes, includes, err := d.Parse()
func NewDockerignoreContext(app *app.App) (*DockerignoreContext, error) {
hasFile := app.HasFile(".dockerignore")
excludes, includes, err := CheckAndParseDockerignore(app)
if err != nil {
return nil, nil, err
return nil, err
}

if excludes != nil || includes != nil {
logger.LogInfo("Found .dockerignore file, applying filters")
if excludes != nil {
excludes = utils.RemoveDuplicates(excludes)
}

return excludes, includes, nil
if includes != nil {
includes = utils.RemoveDuplicates(includes)
}
return &DockerignoreContext{
Excludes: excludes,
Includes: includes,
HasFile: hasFile,
}, nil
}
130 changes: 68 additions & 62 deletions core/plan/dockerignore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,55 +137,26 @@ func TestDockerignoreContext(t *testing.T) {
testApp, err := app.NewApp(tempDir)
require.NoError(t, err)

ctx := NewDockerignoreContext(testApp)
require.NotNil(t, ctx)
require.Equal(t, testApp, ctx.app)
require.False(t, ctx.parsed)
require.Nil(t, ctx.excludes)
require.Nil(t, ctx.includes)
})

t.Run("parse caching", func(t *testing.T) {
examplePath := filepath.Join("..", "..", "examples", "dockerignore")
testApp, err := app.NewApp(examplePath)
ctx, err := NewDockerignoreContext(testApp)
require.NoError(t, err)

ctx := NewDockerignoreContext(testApp)

// First parse
excludes1, includes1, err1 := ctx.Parse()
require.NoError(t, err1)
require.True(t, ctx.parsed)

// Second parse should return cached results
excludes2, includes2, err2 := ctx.Parse()
require.NoError(t, err2)
require.Equal(t, excludes1, excludes2)
require.Equal(t, includes1, includes2)
require.NotNil(t, ctx)
require.False(t, ctx.HasFile)
require.Nil(t, ctx.Excludes)
require.Nil(t, ctx.Includes)
})

t.Run("parse with logging", func(t *testing.T) {
t.Run("context with dockerignore file", func(t *testing.T) {
examplePath := filepath.Join("..", "..", "examples", "dockerignore")
testApp, err := app.NewApp(examplePath)
require.NoError(t, err)

ctx := NewDockerignoreContext(testApp)

// Mock logger that captures calls
logCalls := []string{}
mockLogger := &mockLogger{logFunc: func(format string, args ...interface{}) {
logCalls = append(logCalls, format)
}}

excludes, includes, err := ctx.ParseWithLogging(mockLogger)
ctx, err := NewDockerignoreContext(testApp)
require.NoError(t, err)
require.NotNil(t, excludes)
require.NotNil(t, includes)
require.Contains(t, includes, "negation_test/should_exist.txt")
require.Contains(t, includes, "negation_test/existing_folder")

// Should have logged that dockerignore was found
require.Contains(t, logCalls, "Found .dockerignore file, applying filters")
require.True(t, ctx.HasFile)
require.NotNil(t, ctx.Excludes)
require.NotNil(t, ctx.Includes)
require.Contains(t, ctx.Includes, "negation_test/should_exist.txt")
require.Contains(t, ctx.Includes, "negation_test/existing_folder")
})

t.Run("parse nonexistent file", func(t *testing.T) {
Expand All @@ -196,13 +167,11 @@ func TestDockerignoreContext(t *testing.T) {
testApp, err := app.NewApp(tempDir)
require.NoError(t, err)

ctx := NewDockerignoreContext(testApp)

excludes, includes, err := ctx.Parse()
ctx, err := NewDockerignoreContext(testApp)
require.NoError(t, err)
require.Nil(t, excludes)
require.Nil(t, includes)
require.True(t, ctx.parsed) // Should still mark as parsed
require.False(t, ctx.HasFile)
require.Nil(t, ctx.Excludes)
require.Nil(t, ctx.Includes)
})

t.Run("parse error handling", func(t *testing.T) {
Expand All @@ -223,25 +192,62 @@ func TestDockerignoreContext(t *testing.T) {
testApp, err := app.NewApp(tempDir)
require.NoError(t, err)

ctx := NewDockerignoreContext(testApp)
excludes, includes, err := ctx.Parse()

ctx, err := NewDockerignoreContext(testApp)
require.Error(t, err)
require.Nil(t, excludes)
require.Nil(t, includes)
require.False(t, ctx.parsed) // Should not mark as parsed on error
require.Nil(t, ctx)
})
}

// Mock logger for testing
type mockLogger struct {
logFunc func(string, ...interface{})
}
func TestDockerignoreDuplicatePatterns(t *testing.T) {
t.Run("duplicate patterns removed", func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "dockerignore-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

// Create test files
err = os.WriteFile(filepath.Join(tempDir, "keep.txt"), []byte("exists"), 0644)
require.NoError(t, err)

// Create .dockerignore with duplicate patterns
dockerignoreContent := `*.log
*.log
node_modules
!keep.txt
!keep.txt
`
err = os.WriteFile(filepath.Join(tempDir, ".dockerignore"), []byte(dockerignoreContent), 0644)
require.NoError(t, err)

func (m *mockLogger) LogInfo(format string, args ...interface{}) {
if m.logFunc != nil {
m.logFunc(format, args...)
}
testApp, err := app.NewApp(tempDir)
require.NoError(t, err)

ctx, err := NewDockerignoreContext(testApp)
require.NoError(t, err)

// Count occurrences of each pattern
logCount := 0
nodeModulesCount := 0
for _, pattern := range ctx.Excludes {
if pattern == "*.log" {
logCount++
}
if pattern == "node_modules" {
nodeModulesCount++
}
}

keepCount := 0
for _, pattern := range ctx.Includes {
if pattern == "keep.txt" {
keepCount++
}
}

// Verify no duplicates exist
require.LessOrEqual(t, logCount, 1, "*.log pattern should appear at most once")
require.LessOrEqual(t, nodeModulesCount, 1, "node_modules pattern should appear at most once")
require.LessOrEqual(t, keepCount, 1, "keep.txt pattern should appear at most once")
})
}

func TestCheckAndParseDockerignoreWithNegation(t *testing.T) {
Expand All @@ -259,7 +265,7 @@ func TestCheckAndParseDockerignoreWithNegation(t *testing.T) {
require.NoError(t, err)

// Create .dockerignore with mixed negation cases
dockerignoreContent := `
dockerignoreContent := `
negation_test/*
!negation_test/should_exist.txt
!negation_test/should_not_exist.txt
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ title: Running Railpack in Production
description: Learn how to run Railpack in production as a platform
---

This guide will walk you through running Railpack in production as a platform
(like Railway).
This guide walks through running Railpack in production as a platform
(like Railway). This guide is not intended for end users using Railpack to
containerize their applications.

## CLI vs Frontend

Expand Down