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
24 changes: 8 additions & 16 deletions backend/internal/services/project_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1759,14 +1759,6 @@ func resolveDockerfilePathInternal(svc composetypes.ServiceConfig) (string, erro
return dockerfilePath, nil
}

func translateBuildPathInternal(path string, pathMapper *projects.PathMapper) (string, error) {
if pathMapper == nil || strings.TrimSpace(path) == "" || !filepath.IsAbs(path) {
return path, nil
}

return pathMapper.ContainerToHost(path)
}

func buildArgsFromCompose(args map[string]*string) map[string]string {
buildArgs := map[string]string{}
for key, value := range args {
Expand Down Expand Up @@ -1895,14 +1887,18 @@ func (s *ProjectService) prepareServiceBuildRequest(
return imagetypes.BuildRequest{}, updatedSvc, updated, fmt.Errorf("service %s must define an image when push is enabled", serviceName)
}

// The build context (and any absolute Dockerfile path) is read locally by
// Arcane — both the docker provider (`archive.TarWithOptions`) and the
// buildkit provider (`SolveOpt.LocalDirs`) stream the directory contents
// to the daemon from the Arcane process's own filesystem. It must
// therefore stay as a container path; translating it to the host path
// (which is what bind mount sources need) makes `os.Stat` fail because
// the host path doesn't exist inside the Arcane container. See #2314.
// pathMapper is intentionally not consumed here for that reason.
contextDir, err := resolveBuildContextInternal(project.WorkingDir, updatedSvc, serviceName)
if err != nil {
return imagetypes.BuildRequest{}, updatedSvc, updated, err
}
contextDir, err = translateBuildPathInternal(contextDir, pathMapper)
if err != nil {
return imagetypes.BuildRequest{}, updatedSvc, updated, fmt.Errorf("translate build context for service %s: %w", serviceName, err)
}

dockerfileInline := updatedSvc.Build.DockerfileInline
if strings.TrimSpace(updatedSvc.Build.Dockerfile) != "" && strings.TrimSpace(dockerfileInline) != "" {
Expand All @@ -1915,10 +1911,6 @@ func (s *ProjectService) prepareServiceBuildRequest(
if err != nil {
return imagetypes.BuildRequest{}, updatedSvc, updated, err
}
dockerfilePath, err = translateBuildPathInternal(dockerfilePath, pathMapper)
if err != nil {
return imagetypes.BuildRequest{}, updatedSvc, updated, fmt.Errorf("translate Dockerfile path for service %s: %w", serviceName, err)
}
}

buildReq := imagetypes.BuildRequest{
Expand Down
46 changes: 43 additions & 3 deletions backend/internal/services/project_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,13 @@ func TestProjectService_PrepareServiceBuildRequest_MapsComposeFields(t *testing.
assert.Contains(t, req.ExtraHosts[0], "10.0.0.5")
}

func TestProjectService_PrepareServiceBuildRequest_UsesExecutorVisiblePaths(t *testing.T) {
// TestProjectService_PrepareServiceBuildRequest_KeepsContainerPaths is a
// regression test for #2314: Arcane's local build pipeline (the docker and
// buildkit providers both read the build context via the Arcane process's own
// filesystem) cannot use host paths, so prepareServiceBuildRequest must leave
// the build context and any absolute Dockerfile path as container paths even
// when the projects mount has a non-matching host prefix.
func TestProjectService_PrepareServiceBuildRequest_KeepsContainerPaths(t *testing.T) {
svc := &ProjectService{}
proj := &composetypes.Project{WorkingDir: "/app/data/projects/demo", Name: "demo"}
pm := projects.NewPathMapper("/app/data/projects", "/docker-data/arcane/projects")
Expand All @@ -1461,8 +1467,42 @@ func TestProjectService_PrepareServiceBuildRequest_UsesExecutorVisiblePaths(t *t
)
require.NoError(t, err)

assert.Equal(t, "/docker-data/arcane/projects/demo", req.ContextDir)
assert.Equal(t, "/docker-data/arcane/projects/demo/Dockerfile.custom", req.Dockerfile)
assert.Equal(t, "/app/data/projects/demo", req.ContextDir)
assert.Equal(t, "/app/data/projects/demo/Dockerfile.custom", req.Dockerfile)
}

// TestProjectService_PrepareServiceBuildRequest_BuildDotKeepsContainerPath
// reproduces the exact configuration from #2314: a compose file with
// `build: .` next to its Dockerfile, on an installation where the projects
// directory is bind-mounted from a different host path than the container
// path. The resulting BuildRequest must point at the container path so the
// local builder can stat / tar the directory.
func TestProjectService_PrepareServiceBuildRequest_BuildDotKeepsContainerPath(t *testing.T) {
svc := &ProjectService{}
proj := &composetypes.Project{WorkingDir: "/app/data/projects/caddy", Name: "caddy"}
pm := projects.NewPathMapper("/app/data/projects", "/storage/volumes/arcane/projects")

serviceCfg := composetypes.ServiceConfig{
Name: "caddy",
Image: "caddy",
Build: &composetypes.BuildConfig{
Context: ".",
},
}

req, _, _, err := svc.prepareServiceBuildRequest(
context.Background(),
"project-id",
proj,
"caddy",
serviceCfg,
ProjectBuildOptions{},
pm,
)
require.NoError(t, err)

assert.Equal(t, "/app/data/projects/caddy", req.ContextDir)
assert.Equal(t, "Dockerfile", req.Dockerfile)
}

func TestProjectService_PrepareServiceBuildRequest_UsesInlineDockerfile(t *testing.T) {
Expand Down
Loading