From a5bacfa86f68a249569d0a365b2a2d63f05ea2f7 Mon Sep 17 00:00:00 2001 From: Renuka Fernando Date: Thu, 16 Jul 2026 18:39:26 +0530 Subject: [PATCH] fix(gateway-builder,cli): stage python-executor inside builder container When using a rootful Docker daemon on Linux, the gateway-builder container runs as root and creates output/ files owned by root:root. The CLI's post-build steps (ensurePythonExecutorInRuntimeContext, ensureBuildLockInControllerContext) then fail with permission denied when trying to write into those root-owned directories. - Move python-executor staging into gateway-builder (main.go) so all file operations happen inside the container where root has full access - Add fsutil.CopyDir to the shared fsutil package (eliminates the duplicate private copyDir in policyengine/generator.go) - Remove ensurePythonExecutorInRuntimeContext and ensureBuildLockInControllerContext from the CLI entirely; the CLI no longer touches the output directory after the builder exits Fixes: wso2/api-platform#2675 --- cli/src/internal/gateway/docker_build.go | 53 ------------------- gateway/gateway-builder/cmd/builder/main.go | 14 +++++ .../pkg/fsutil/path_validation.go | 22 ++++++++ 3 files changed, 36 insertions(+), 53 deletions(-) diff --git a/cli/src/internal/gateway/docker_build.go b/cli/src/internal/gateway/docker_build.go index 96245c9883..e772dd2fae 100644 --- a/cli/src/internal/gateway/docker_build.go +++ b/cli/src/internal/gateway/docker_build.go @@ -19,7 +19,6 @@ package gateway import ( - "errors" "fmt" "os" "os/exec" @@ -70,14 +69,6 @@ func BuildGatewayImages(config DockerBuildConfig) error { } } - if err := ensureBuildLockInControllerContext(config.TempDir); err != nil { - return err - } - - if err := ensurePythonExecutorInRuntimeContext(config.TempDir); err != nil { - return err - } - fmt.Println(" ✓ Gateway-builder completed") // Step 2: Build the two images @@ -105,50 +96,6 @@ func BuildGatewayImages(config DockerBuildConfig) error { return nil } -func ensureBuildLockInControllerContext(tempDir string) error { - buildManifestSrc := filepath.Join(tempDir, "build-manifest.yaml") - if _, err := os.Stat(buildManifestSrc); err != nil { - return fmt.Errorf("build-manifest.yaml not found in gateway-builder output: %w", err) - } - - buildManifestDst := filepath.Join(tempDir, "output", "gateway-controller", "build-manifest.yaml") - if _, err := os.Stat(buildManifestDst); err == nil { - return nil - } else if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("failed to access gateway-controller build-manifest.yaml: %w", err) - } - - if err := utils.CopyFile(buildManifestSrc, buildManifestDst); err != nil { - return fmt.Errorf("failed to copy build-manifest.yaml into gateway-controller build context: %w", err) - } - - return nil -} - -// ensurePythonExecutorInRuntimeContext stages the python-executor directory -// produced by gateway-builder into the gateway-runtime Docker build context -// so the Dockerfile's `COPY python-executor/ ...` can resolve it. -func ensurePythonExecutorInRuntimeContext(tempDir string) error { - pythonExecutorSrc := filepath.Join(tempDir, "output", "python-executor") - if _, err := os.Stat(pythonExecutorSrc); err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil - } - return fmt.Errorf("failed to access python-executor output: %w", err) - } - - pythonExecutorDst := filepath.Join(tempDir, "output", "gateway-runtime", "python-executor") - if err := os.RemoveAll(pythonExecutorDst); err != nil { - return fmt.Errorf("failed to clear existing python-executor in gateway-runtime build context: %w", err) - } - - if err := utils.CopyDir(pythonExecutorSrc, pythonExecutorDst); err != nil { - return fmt.Errorf("failed to copy python-executor into gateway-runtime build context: %w", err) - } - - return nil -} - // runGatewayBuilder runs the gateway-builder container func runGatewayBuilder(config DockerBuildConfig, logFile *os.File) error { args := []string{"run", "--rm", "-v", config.TempDir + ":/workspace", config.GatewayBuilder, diff --git a/gateway/gateway-builder/cmd/builder/main.go b/gateway/gateway-builder/cmd/builder/main.go index 098c5e79f4..2631e07108 100644 --- a/gateway/gateway-builder/cmd/builder/main.go +++ b/gateway/gateway-builder/cmd/builder/main.go @@ -255,6 +255,20 @@ func main() { errors.FatalError(errors.NewDockerError("Dockerfile generation failed", err)) } + // Stage python-executor into the gateway-runtime build context so that the + // Dockerfile's `COPY python-executor/ ...` directive resolves correctly when the + // CLI later runs `docker build` on the output directory. Doing this here (inside + // the container) avoids a host-side file operation on root-owned output files, + // which fails with permission denied when using a rootful Docker daemon on Linux. + pythonExecutorSrc := filepath.Join(*outputDir, "python-executor") + if _, statErr := os.Stat(pythonExecutorSrc); statErr == nil { + pythonExecutorDst := filepath.Join(*outputDir, "gateway-runtime", "python-executor") + if err := fsutil.CopyDir(pythonExecutorSrc, pythonExecutorDst); err != nil { + errors.FatalError(errors.NewGenerationError("failed to stage python-executor into gateway-runtime build context", err)) + } + slog.Info("Staged python-executor into gateway-runtime build context", "dst", pythonExecutorDst) + } + // Phase 6: Build Info Generation slog.Info("Starting Phase 6: Build Info Generation", "phase", "build-info") diff --git a/gateway/gateway-builder/pkg/fsutil/path_validation.go b/gateway/gateway-builder/pkg/fsutil/path_validation.go index 198ae3798e..5ad028834a 100644 --- a/gateway/gateway-builder/pkg/fsutil/path_validation.go +++ b/gateway/gateway-builder/pkg/fsutil/path_validation.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "os" + "path/filepath" ) // ValidatePathExists checks if a file or directory exists and is accessible. @@ -52,6 +53,27 @@ func ValidatePathExists(path string, pathType string) error { return fmt.Errorf("failed to access %s: %s: %w", pathType, path, err) } +// CopyDir recursively copies src into dst, preserving file modes. +func CopyDir(src, dst string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + target := filepath.Join(dst, rel) + if info.IsDir() { + return os.MkdirAll(target, info.Mode()) + } + if !info.Mode().IsRegular() { + return nil + } + return CopyFile(path, target) + }) +} + // CopyFile copies a file from src to dst func CopyFile(src, dst string) error { sourceFile, err := os.Open(src)