Skip to content

Commit 30f4653

Browse files
authored
Fall back to Dockerfile for Rails scanner if bundler or ruby errors occur (#4651)
1 parent e1b577e commit 30f4653

File tree

2 files changed

+285
-9
lines changed

2 files changed

+285
-9
lines changed

scanner/rails.go

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ func configureRails(sourceDir string, config *ScannerConfig) (*SourceInfo, error
4444
}
4545

4646
if err != nil {
47-
return nil, errors.Wrap(err, "failure finding bundle executable")
47+
// If bundle is not found but a Dockerfile exists, we can still proceed
48+
if _, statErr := os.Stat(filepath.Join(sourceDir, "Dockerfile")); statErr == nil {
49+
fmt.Printf("Detected existing Dockerfile, will use it for Rails app (bundle not found)\n")
50+
bundle = "" // Mark as unavailable
51+
} else {
52+
return nil, errors.Wrap(err, "failure finding bundle executable")
53+
}
4854
}
4955
}
5056

@@ -55,7 +61,13 @@ func configureRails(sourceDir string, config *ScannerConfig) (*SourceInfo, error
5561
}
5662

5763
if err != nil {
58-
return nil, errors.Wrap(err, "failure finding ruby executable")
64+
// If ruby is not found but a Dockerfile exists, we can still proceed
65+
if _, statErr := os.Stat(filepath.Join(sourceDir, "Dockerfile")); statErr == nil {
66+
fmt.Printf("Detected existing Dockerfile, will use it for Rails app (ruby not found)\n")
67+
ruby = "" // Mark as unavailable
68+
} else {
69+
return nil, errors.Wrap(err, "failure finding ruby executable")
70+
}
5971
}
6072
}
6173

@@ -307,6 +319,39 @@ func RailsCallback(appName string, srcInfo *SourceInfo, plan *plan.LaunchPlan, f
307319
// If the generator fails but a Dockerfile exists, warn the user and proceed. Only fail if no
308320
// Dockerfile exists at the end of this process.
309321

322+
// If bundle or ruby are not available, check if Dockerfile exists and skip generator
323+
if bundle == "" || ruby == "" {
324+
if _, err := os.Stat("Dockerfile"); err == nil {
325+
fmt.Printf("Using existing Dockerfile (bundle/ruby not available locally)\n")
326+
// Read and parse the existing Dockerfile to extract configuration
327+
if dockerfile, err := os.ReadFile("Dockerfile"); err == nil {
328+
// Extract port from Dockerfile
329+
re := regexp.MustCompile(`(?m)^EXPOSE\s+(?P<port>\d+)`)
330+
m := re.FindStringSubmatch(string(dockerfile))
331+
if len(m) > 0 {
332+
if port, err := strconv.Atoi(m[1]); err == nil {
333+
srcInfo.Port = port
334+
}
335+
}
336+
337+
// Extract volume
338+
reVol := regexp.MustCompile(`(?m)^VOLUME\s+(\[\s*")?(\/[\w\/]*?(\w+))("\s*\])?\s*$`)
339+
mVol := reVol.FindStringSubmatch(string(dockerfile))
340+
if len(mVol) > 0 {
341+
srcInfo.Volumes = []Volume{
342+
{
343+
Source: mVol[3],
344+
Destination: mVol[2],
345+
},
346+
}
347+
}
348+
}
349+
return nil
350+
} else {
351+
return errors.New("No Dockerfile found and bundle/ruby not available to generate one")
352+
}
353+
}
354+
310355
// install dockerfile-rails gem, if not already included and the gem directory is writable
311356
// if an error occurrs, store it for later in pendingError
312357
generatorInstalled := false
@@ -359,15 +404,46 @@ func RailsCallback(appName string, srcInfo *SourceInfo, plan *plan.LaunchPlan, f
359404

360405
err = cmd.Run()
361406
if err != nil {
362-
return errors.Wrap(err, "Failed to install bundle, exiting")
407+
// Check if a Dockerfile already exists - if so, we can proceed without bundle
408+
if dockerfile, statErr := os.ReadFile("Dockerfile"); statErr == nil {
409+
fmt.Printf("Detected existing Dockerfile, will use it for Rails app (skipping dockerfile-rails generator)\n")
410+
411+
// Extract port from existing Dockerfile
412+
re := regexp.MustCompile(`(?m)^EXPOSE\s+(?P<port>\d+)`)
413+
m := re.FindStringSubmatch(string(dockerfile))
414+
if len(m) > 0 {
415+
if port, parseErr := strconv.Atoi(m[1]); parseErr == nil {
416+
srcInfo.Port = port
417+
}
418+
}
419+
420+
// Extract volume from existing Dockerfile
421+
reVol := regexp.MustCompile(`(?m)^VOLUME\s+(\[\s*")?(\/[\w\/]*?(\w+))("\s*\])?\s*$`)
422+
mVol := reVol.FindStringSubmatch(string(dockerfile))
423+
if len(mVol) > 0 {
424+
srcInfo.Volumes = []Volume{
425+
{
426+
Source: mVol[3],
427+
Destination: mVol[2],
428+
},
429+
}
430+
}
431+
432+
// Successfully using existing Dockerfile, return without running generator
433+
return nil
434+
} else {
435+
return errors.Wrap(err, "Failed to install bundle, exiting")
436+
}
363437
}
364438

365-
// ensure Gemfile.lock includes the x86_64-linux platform
366-
if out, err := exec.Command(bundle, "platform").Output(); err == nil {
367-
if !strings.Contains(string(out), "x86_64-linux") {
368-
cmd := exec.Command(bundle, "lock", "--add-platform", "x86_64-linux")
369-
if err := cmd.Run(); err != nil {
370-
return errors.Wrap(err, "Failed to add x86_64-linux platform, exiting")
439+
// ensure Gemfile.lock includes the x86_64-linux platform (skip if bundle install failed)
440+
if pendingError == nil {
441+
if out, err := exec.Command(bundle, "platform").Output(); err == nil {
442+
if !strings.Contains(string(out), "x86_64-linux") {
443+
cmd := exec.Command(bundle, "lock", "--add-platform", "x86_64-linux")
444+
if err := cmd.Run(); err != nil {
445+
return errors.Wrap(err, "Failed to add x86_64-linux platform, exiting")
446+
}
371447
}
372448
}
373449
}

scanner/rails_dockerfile_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package scanner
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestRailsScannerWithExistingDockerfile(t *testing.T) {
13+
t.Run("uses existing Dockerfile when bundle install fails", func(t *testing.T) {
14+
dir := t.TempDir()
15+
16+
// Create a Rails app structure
17+
err := os.WriteFile(filepath.Join(dir, "Gemfile"), []byte("source 'https://rubygems.org'\ngem 'rails', '~> 7.1.0'"), 0644)
18+
require.NoError(t, err)
19+
20+
err = os.WriteFile(filepath.Join(dir, "Gemfile.lock"), []byte("GEM\n remote: https://rubygems.org/\n specs:\n rails (7.1.0)"), 0644)
21+
require.NoError(t, err)
22+
23+
err = os.WriteFile(filepath.Join(dir, "config.ru"), []byte("require_relative 'config/environment'\nrun Rails.application"), 0644)
24+
require.NoError(t, err)
25+
26+
// Create a custom Dockerfile with identifiable content
27+
customDockerfile := `FROM ruby:3.2.2
28+
WORKDIR /app
29+
COPY . .
30+
EXPOSE 3000
31+
CMD ["rails", "server"]
32+
# CUSTOM MARKER: This is a custom Dockerfile`
33+
err = os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(customDockerfile), 0644)
34+
require.NoError(t, err)
35+
36+
// Change to test directory
37+
originalDir, _ := os.Getwd()
38+
defer os.Chdir(originalDir)
39+
err = os.Chdir(dir)
40+
require.NoError(t, err)
41+
42+
// Run the scanner - it should detect the Rails app
43+
si, err := configureRails(dir, &ScannerConfig{})
44+
45+
// The scanner should succeed in detecting Rails
46+
require.NoError(t, err)
47+
require.NotNil(t, si)
48+
assert.Equal(t, "Rails", si.Family)
49+
50+
// Verify the Dockerfile still exists and wasn't modified
51+
dockerfileContent, err := os.ReadFile(filepath.Join(dir, "Dockerfile"))
52+
require.NoError(t, err)
53+
assert.Contains(t, string(dockerfileContent), "CUSTOM MARKER", "Custom Dockerfile should be preserved")
54+
assert.Equal(t, customDockerfile, string(dockerfileContent), "Dockerfile should be unchanged")
55+
56+
// The callback would normally be called during launch, but we can't easily test that
57+
// without bundle/ruby being available. The key is that configureRails doesn't fail.
58+
})
59+
60+
t.Run("extracts port from existing Dockerfile", func(t *testing.T) {
61+
dir := t.TempDir()
62+
63+
// Create minimal Rails files
64+
err := os.WriteFile(filepath.Join(dir, "Gemfile"), []byte("source 'https://rubygems.org'\ngem 'rails'"), 0644)
65+
require.NoError(t, err)
66+
67+
err = os.WriteFile(filepath.Join(dir, "config.ru"), []byte("run Rails.application"), 0644)
68+
require.NoError(t, err)
69+
70+
// Create Dockerfile with custom port
71+
customDockerfile := `FROM ruby:3.2
72+
WORKDIR /app
73+
EXPOSE 8080
74+
CMD ["rails", "server"]`
75+
err = os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(customDockerfile), 0644)
76+
require.NoError(t, err)
77+
78+
originalDir, _ := os.Getwd()
79+
defer os.Chdir(originalDir)
80+
err = os.Chdir(dir)
81+
require.NoError(t, err)
82+
83+
si, err := configureRails(dir, &ScannerConfig{})
84+
require.NoError(t, err)
85+
require.NotNil(t, si)
86+
87+
// The port extraction happens in RailsCallback when bundle install fails
88+
// For now, just verify the scanner doesn't fail with an existing Dockerfile
89+
assert.Equal(t, "Rails", si.Family)
90+
})
91+
92+
t.Run("extracts volume from existing Dockerfile", func(t *testing.T) {
93+
dir := t.TempDir()
94+
95+
// Create minimal Rails files
96+
err := os.WriteFile(filepath.Join(dir, "Gemfile"), []byte("source 'https://rubygems.org'\ngem 'rails'"), 0644)
97+
require.NoError(t, err)
98+
99+
err = os.WriteFile(filepath.Join(dir, "config.ru"), []byte("run Rails.application"), 0644)
100+
require.NoError(t, err)
101+
102+
// Create Dockerfile with volume
103+
customDockerfile := `FROM ruby:3.2
104+
WORKDIR /app
105+
VOLUME /app/storage
106+
EXPOSE 3000
107+
CMD ["rails", "server"]`
108+
err = os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(customDockerfile), 0644)
109+
require.NoError(t, err)
110+
111+
originalDir, _ := os.Getwd()
112+
defer os.Chdir(originalDir)
113+
err = os.Chdir(dir)
114+
require.NoError(t, err)
115+
116+
si, err := configureRails(dir, &ScannerConfig{})
117+
require.NoError(t, err)
118+
require.NotNil(t, si)
119+
120+
// The volume extraction happens in RailsCallback when bundle install fails
121+
// For now, just verify the scanner doesn't fail with an existing Dockerfile
122+
assert.Equal(t, "Rails", si.Family)
123+
})
124+
125+
t.Run("fails when no Dockerfile exists and bundle not available", func(t *testing.T) {
126+
dir := t.TempDir()
127+
128+
// Create minimal Rails files but NO Dockerfile
129+
err := os.WriteFile(filepath.Join(dir, "Gemfile"), []byte("source 'https://rubygems.org'\ngem 'rails'"), 0644)
130+
require.NoError(t, err)
131+
132+
err = os.WriteFile(filepath.Join(dir, "config.ru"), []byte("run Rails.application"), 0644)
133+
require.NoError(t, err)
134+
135+
// Note: No Dockerfile created
136+
137+
originalDir, _ := os.Getwd()
138+
defer os.Chdir(originalDir)
139+
err = os.Chdir(dir)
140+
require.NoError(t, err)
141+
142+
// This test would need bundle to not be available, which is hard to simulate
143+
// The scanner will either find bundle (and try to use it) or not find it
144+
// If bundle is not found and no Dockerfile exists, it should fail
145+
146+
// For now, we just verify that the scanner can detect Rails
147+
si, err := configureRails(dir, &ScannerConfig{})
148+
149+
// If bundle IS available locally, this will succeed
150+
// If bundle is NOT available and no Dockerfile exists, this should fail
151+
// We can't reliably test both cases, so we just verify it doesn't panic
152+
if err != nil {
153+
// Expected when bundle not available and no Dockerfile
154+
assert.Contains(t, err.Error(), "bundle")
155+
} else if si != nil {
156+
// Expected when bundle is available
157+
assert.Equal(t, "Rails", si.Family)
158+
}
159+
})
160+
}
161+
162+
func TestRailsScannerPreservesDockerfileWithBin(t *testing.T) {
163+
t.Run("detects Rails via bin/rails and preserves Dockerfile", func(t *testing.T) {
164+
dir := t.TempDir()
165+
166+
// Create bin directory with rails script
167+
binDir := filepath.Join(dir, "bin")
168+
err := os.MkdirAll(binDir, 0755)
169+
require.NoError(t, err)
170+
171+
err = os.WriteFile(filepath.Join(binDir, "rails"), []byte("#!/usr/bin/env ruby\n# Rails script"), 0755)
172+
require.NoError(t, err)
173+
174+
// Create Gemfile
175+
err = os.WriteFile(filepath.Join(dir, "Gemfile"), []byte("source 'https://rubygems.org'\ngem 'rails'"), 0644)
176+
require.NoError(t, err)
177+
178+
// Create custom Dockerfile
179+
customDockerfile := `FROM ruby:3.2
180+
# Custom Rails Dockerfile
181+
EXPOSE 3000`
182+
err = os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte(customDockerfile), 0644)
183+
require.NoError(t, err)
184+
185+
originalDir, _ := os.Getwd()
186+
defer os.Chdir(originalDir)
187+
err = os.Chdir(dir)
188+
require.NoError(t, err)
189+
190+
si, err := configureRails(dir, &ScannerConfig{})
191+
require.NoError(t, err)
192+
require.NotNil(t, si)
193+
assert.Equal(t, "Rails", si.Family)
194+
195+
// Verify Dockerfile wasn't modified
196+
dockerfileContent, err := os.ReadFile(filepath.Join(dir, "Dockerfile"))
197+
require.NoError(t, err)
198+
assert.Equal(t, customDockerfile, string(dockerfileContent))
199+
})
200+
}

0 commit comments

Comments
 (0)