Skip to content

Commit 8637855

Browse files
authored
Merge pull request #101 from flatrun/fix/bind-mount-file-detection
fix(docker): Distinguish file from directory bind mounts
2 parents 05645be + b2d4342 commit 8637855

7 files changed

Lines changed: 82 additions & 20 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.52
1+
0.1.53

cmd/agent/setup.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,16 @@ import (
1616
)
1717

1818
type templateMetadata struct {
19-
Name string `yaml:"name"`
20-
Type string `yaml:"type"`
21-
Category string `yaml:"category"`
22-
Setup setupManifest `yaml:"setup"`
19+
Name string `yaml:"name"`
20+
Type string `yaml:"type"`
21+
Category string `yaml:"category"`
22+
Mounts mountsManifest `yaml:"mounts"`
23+
Setup setupManifest `yaml:"setup"`
24+
}
25+
26+
type mountsManifest struct {
27+
Dirs []string `yaml:"dirs"`
28+
Files []string `yaml:"files"`
2329
}
2430

2531
type setupManifest struct {
@@ -172,11 +178,6 @@ func deployInfraService(cfg *config.Config, serviceName, templateID string) erro
172178
content = strings.ReplaceAll(content, "${NAME}", serviceName)
173179
content = strings.ReplaceAll(content, "${PROXY_NETWORK}", cfg.Infrastructure.DefaultProxyNetwork)
174180

175-
manager := docker.NewManager(cfg.DeploymentsPath)
176-
if err := manager.CreateDeployment(serviceName, content); err != nil {
177-
return fmt.Errorf("create deployment: %w", err)
178-
}
179-
180181
meta, err := loadInfraMetadata(templateID)
181182
if err != nil {
182183
return fmt.Errorf("load metadata: %w", err)
@@ -186,6 +187,11 @@ func deployInfraService(cfg *config.Config, serviceName, templateID string) erro
186187
return fmt.Errorf("write template files: %w", err)
187188
}
188189

190+
manager := docker.NewManager(cfg.DeploymentsPath)
191+
if err := manager.CreateDeployment(serviceName, content, meta.Mounts.Files); err != nil {
192+
return fmt.Errorf("create deployment: %w", err)
193+
}
194+
189195
netManager := networks.NewManager()
190196
if err := netManager.EnsureNetwork(cfg.Infrastructure.DefaultProxyNetwork); err != nil {
191197
return fmt.Errorf("ensure proxy network: %w", err)

internal/api/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ func (s *Server) createDeployment(c *gin.Context) {
826826
req.ComposeContent = s.addContainerNetwork(req.ComposeContent, req.ExistingDatabaseContainer)
827827
}
828828

829-
if err := s.manager.CreateDeployment(req.Name, req.ComposeContent); err != nil {
829+
if err := s.manager.CreateDeployment(req.Name, req.ComposeContent, nil); err != nil {
830830
c.JSON(http.StatusInternalServerError, gin.H{
831831
"error": err.Error(),
832832
})

internal/docker/discovery.go

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ func (d *Discovery) loadMetadata(path string) (*models.ServiceMetadata, error) {
235235
return &metadata, nil
236236
}
237237

238-
func (d *Discovery) CreateDeployment(name string, composeContent string) error {
238+
func (d *Discovery) CreateDeployment(name string, composeContent string, fileMounts []string) error {
239239
dirPath := filepath.Join(d.basePath, name)
240240

241241
if err := os.MkdirAll(dirPath, 0755); err != nil {
@@ -246,7 +246,7 @@ func (d *Discovery) CreateDeployment(name string, composeContent string) error {
246246
composeContent = d.ensureComposeName(name, composeContent)
247247

248248
// Pre-create bind mount directories with permissive access for non-root containers
249-
if err := d.createBindMountDirs(dirPath, composeContent); err != nil {
249+
if err := d.createBindMountDirs(dirPath, composeContent, fileMounts); err != nil {
250250
return fmt.Errorf("failed to create mount directories: %w", err)
251251
}
252252

@@ -255,8 +255,11 @@ func (d *Discovery) CreateDeployment(name string, composeContent string) error {
255255
}
256256

257257
// createBindMountDirs parses compose content and creates bind mount directories
258-
// with world-writable permissions to support non-root containers (e.g., Bitnami)
259-
func (d *Discovery) createBindMountDirs(deploymentPath, composeContent string) error {
258+
// with world-writable permissions to support non-root containers (e.g., Bitnami).
259+
// fileMounts lists relative paths (e.g., "./nginx.conf") that are file mounts
260+
// from template metadata. For paths not in fileMounts, a basename-contains-dot
261+
// heuristic is used as a fallback to avoid creating files as directories.
262+
func (d *Discovery) createBindMountDirs(deploymentPath, composeContent string, fileMounts []string) error {
260263
var compose struct {
261264
Services map[string]struct {
262265
Volumes []string `yaml:"volumes"`
@@ -267,19 +270,41 @@ func (d *Discovery) createBindMountDirs(deploymentPath, composeContent string) e
267270
return nil // Skip if parse fails, not critical
268271
}
269272

273+
fileMountSet := make(map[string]bool, len(fileMounts))
274+
for _, fm := range fileMounts {
275+
cleanPath := filepath.Clean(fm)
276+
fileMountSet[cleanPath] = true
277+
fileMountSet["./"+cleanPath] = true
278+
}
279+
270280
for _, service := range compose.Services {
271281
for _, volume := range service.Volumes {
272282
hostPath := extractBindMountPath(volume)
273283
if hostPath == "" {
274284
continue
275285
}
276286

277-
// Only handle relative paths (bind mounts)
278287
if !strings.HasPrefix(hostPath, "./") && !strings.HasPrefix(hostPath, "../") {
279288
continue
280289
}
281290

282291
fullPath := filepath.Join(deploymentPath, hostPath)
292+
293+
if _, err := os.Stat(fullPath); err == nil {
294+
continue
295+
}
296+
297+
if isFileMount(hostPath, fileMountSet) {
298+
parentDir := filepath.Dir(fullPath)
299+
if err := os.MkdirAll(parentDir, 0777); err != nil {
300+
return err
301+
}
302+
if err := os.Chmod(parentDir, 0777); err != nil {
303+
return err
304+
}
305+
continue
306+
}
307+
283308
if err := os.MkdirAll(fullPath, 0777); err != nil {
284309
return err
285310
}
@@ -292,6 +317,28 @@ func (d *Discovery) createBindMountDirs(deploymentPath, composeContent string) e
292317
return nil
293318
}
294319

320+
// isFileMount checks whether a bind mount host path refers to a file.
321+
// It first checks the metadata-provided fileMounts set, then falls back
322+
// to a heuristic: if the basename contains a dot and is not a known
323+
// directory pattern (e.g., conf.d), it's treated as a file.
324+
func isFileMount(hostPath string, fileMountSet map[string]bool) bool {
325+
if fileMountSet[hostPath] {
326+
return true
327+
}
328+
329+
base := filepath.Base(hostPath)
330+
if !strings.Contains(base, ".") {
331+
return false
332+
}
333+
334+
// Known directory suffixes like .d (conf.d, certs.d, etc.)
335+
if strings.HasSuffix(base, ".d") {
336+
return false
337+
}
338+
339+
return true
340+
}
341+
295342
// extractBindMountPath extracts the host path from a volume mount string
296343
// Handles formats: "./path:/container/path" or "./path:/container/path:ro"
297344
func extractBindMountPath(volume string) string {

internal/docker/discovery_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ services:
148148
t.Fatalf("Failed to create deployment path: %v", err)
149149
}
150150

151-
err := d.createBindMountDirs(deploymentPath, tt.composeContent)
151+
err := d.createBindMountDirs(deploymentPath, tt.composeContent, nil)
152152
if err != nil {
153153
t.Fatalf("createBindMountDirs failed: %v", err)
154154
}
@@ -193,7 +193,7 @@ services:
193193
- "8000"
194194
`
195195

196-
err = d.CreateDeployment("test-app", composeContent)
196+
err = d.CreateDeployment("test-app", composeContent, nil)
197197
if err != nil {
198198
t.Fatalf("CreateDeployment failed: %v", err)
199199
}

internal/docker/manager.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,11 @@ func (m *Manager) populateContainerInfo(deployment *models.Deployment) {
124124
}
125125
}
126126

127-
func (m *Manager) CreateDeployment(name string, composeContent string) error {
127+
func (m *Manager) CreateDeployment(name string, composeContent string, fileMounts []string) error {
128128
m.mu.Lock()
129129
defer m.mu.Unlock()
130130

131-
return m.discovery.CreateDeployment(name, composeContent)
131+
return m.discovery.CreateDeployment(name, composeContent, fileMounts)
132132
}
133133

134134
func (m *Manager) ApplyMountOwnership(name string, mounts []MountOwnership) error {

templates/infra/nginx/metadata.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ category: infrastructure
66
type: infrastructure
77
priority: 100
88

9+
mounts:
10+
dirs:
11+
- conf.d
12+
- certs
13+
- html
14+
- lua
15+
files:
16+
- nginx.conf
17+
918
setup:
1019
dirs:
1120
- conf.d

0 commit comments

Comments
 (0)