From db2ac781a145647a9147df343572eb21b2638ef1 Mon Sep 17 00:00:00 2001 From: IlyaasK <86218345+IlyaasK@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:58:46 +0000 Subject: [PATCH 1/2] Keep configured Dockerfiles outside build context --- lib/builds/builder_agent/main.go | 73 +++++++++++++++++---------- lib/builds/builder_agent/main_test.go | 71 ++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 28 deletions(-) create mode 100644 lib/builds/builder_agent/main_test.go diff --git a/lib/builds/builder_agent/main.go b/lib/builds/builder_agent/main.go index 2540d7e0..f6bc8989 100644 --- a/lib/builds/builder_agent/main.go +++ b/lib/builds/builder_agent/main.go @@ -16,6 +16,7 @@ import ( "encoding/base64" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "log" @@ -489,32 +490,22 @@ func runBuildProcess() { } } - // Ensure Dockerfile exists (either in source or provided via config) - dockerfilePath := filepath.Join(config.SourcePath, "Dockerfile") - if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) { - // Check if Dockerfile was provided in config - if config.Dockerfile == "" { - setResult(BuildResult{ - Success: false, - Error: "Dockerfile required: provide dockerfile parameter or include Dockerfile in source tarball", - Logs: logWriter.String(), - DurationMS: time.Since(start).Milliseconds(), - }) - return - } - // Write provided Dockerfile to source directory - if err := os.WriteFile(dockerfilePath, []byte(config.Dockerfile), 0644); err != nil { - setResult(BuildResult{ - Success: false, - Error: fmt.Sprintf("write dockerfile: %v", err), - Logs: logWriter.String(), - DurationMS: time.Since(start).Milliseconds(), - }) - return - } - log.Println("Using Dockerfile from config") - } else { + dockerfileDir, cleanupDockerfile, err := prepareDockerfile(config) + if err != nil { + setResult(BuildResult{ + Success: false, + Error: err.Error(), + Logs: logWriter.String(), + DurationMS: time.Since(start).Milliseconds(), + }) + return + } + defer cleanupDockerfile() + + if dockerfileDir == config.SourcePath { log.Println("Using Dockerfile from source") + } else { + log.Println("Using Dockerfile from config") } // Compute provenance @@ -522,7 +513,7 @@ func runBuildProcess() { // Run the build log.Println("=== Starting Build ===") - digest, _, err := runBuild(ctx, config, logWriter) + digest, _, err := runBuild(ctx, config, dockerfileDir, logWriter) duration := time.Since(start).Milliseconds() @@ -748,7 +739,33 @@ func setupBuildkitdConfig(config *BuildConfig) error { return nil } -func runBuild(ctx context.Context, config *BuildConfig, logWriter io.Writer) (string, string, error) { +func prepareDockerfile(config *BuildConfig) (string, func(), error) { + dockerfilePath := filepath.Join(config.SourcePath, "Dockerfile") + if _, err := os.Stat(dockerfilePath); err == nil { + return config.SourcePath, func() {}, nil + } else if !os.IsNotExist(err) { + return "", nil, fmt.Errorf("stat dockerfile: %w", err) + } + + if config.Dockerfile == "" { + return "", nil, errors.New("Dockerfile required: provide dockerfile parameter or include Dockerfile in source tarball") + } + + dockerfileDir, err := os.MkdirTemp("", "hypeman-dockerfile-") + if err != nil { + return "", nil, fmt.Errorf("create dockerfile directory: %w", err) + } + cleanup := func() { _ = os.RemoveAll(dockerfileDir) } + + if err := os.WriteFile(filepath.Join(dockerfileDir, "Dockerfile"), []byte(config.Dockerfile), 0644); err != nil { + cleanup() + return "", nil, fmt.Errorf("write dockerfile: %w", err) + } + + return dockerfileDir, cleanup, nil +} + +func runBuild(ctx context.Context, config *BuildConfig, dockerfileDir string, logWriter io.Writer) (string, string, error) { var buildLogs bytes.Buffer // Parse registry host (strip any scheme prefix for backwards compatibility) @@ -781,7 +798,7 @@ func runBuild(ctx context.Context, config *BuildConfig, logWriter io.Writer) (st "build", "--frontend", "dockerfile.v0", "--local", "context=" + config.SourcePath, - "--local", "dockerfile=" + config.SourcePath, + "--local", "dockerfile=" + dockerfileDir, "--output", outputOpts, "--metadata-file", "/tmp/build-metadata.json", } diff --git a/lib/builds/builder_agent/main_test.go b/lib/builds/builder_agent/main_test.go new file mode 100644 index 00000000..012c6bfd --- /dev/null +++ b/lib/builds/builder_agent/main_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func TestPrepareDockerfileKeepsConfiguredDockerfileOutsideSource(t *testing.T) { + sourceDir := t.TempDir() + dockerfile := "FROM alpine:3.21\nCOPY . /app\n" + + dockerfileDir, cleanup, err := prepareDockerfile(&BuildConfig{ + SourcePath: sourceDir, + Dockerfile: dockerfile, + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(cleanup) + + if dockerfileDir == sourceDir { + t.Fatal("configured Dockerfile was written into the source directory") + } + if _, err := os.Stat(filepath.Join(sourceDir, "Dockerfile")); !os.IsNotExist(err) { + t.Fatalf("expected source directory to remain unchanged, got %v", err) + } + + content, err := os.ReadFile(filepath.Join(dockerfileDir, "Dockerfile")) + if err != nil { + t.Fatal(err) + } + if string(content) != dockerfile { + t.Fatalf("unexpected Dockerfile content: %q", content) + } +} + +func TestPrepareDockerfileUsesSourceDockerfile(t *testing.T) { + sourceDir := t.TempDir() + dockerfilePath := filepath.Join(sourceDir, "Dockerfile") + if err := os.WriteFile(dockerfilePath, []byte("FROM source\n"), 0644); err != nil { + t.Fatal(err) + } + + dockerfileDir, cleanup, err := prepareDockerfile(&BuildConfig{ + SourcePath: sourceDir, + Dockerfile: "FROM config\n", + }) + if err != nil { + t.Fatal(err) + } + t.Cleanup(cleanup) + + if dockerfileDir != sourceDir { + t.Fatalf("expected source Dockerfile directory %q, got %q", sourceDir, dockerfileDir) + } + content, err := os.ReadFile(dockerfilePath) + if err != nil { + t.Fatal(err) + } + if string(content) != "FROM source\n" { + t.Fatalf("source Dockerfile was overwritten: %q", content) + } +} + +func TestPrepareDockerfileRequiresDockerfile(t *testing.T) { + _, _, err := prepareDockerfile(&BuildConfig{SourcePath: t.TempDir()}) + if err == nil { + t.Fatal("expected missing Dockerfile to fail") + } +} From 9a5a2020b3ea22baeea5839f6828fbe82d2c9adc Mon Sep 17 00:00:00 2001 From: Ilyaas Kapadia <86218345+IlyaasK@users.noreply.github.com> Date: Wed, 15 Jul 2026 15:08:51 -0400 Subject: [PATCH 2/2] Test BuildKit Dockerfile input separation Extract the initial buildctl arguments and assert configured Dockerfiles use a different local directory from the source context, preventing the cache regression from returning unnoticed. --- lib/builds/builder_agent/main.go | 20 ++++++++++++-------- lib/builds/builder_agent/main_test.go | 19 +++++++++++++++++++ 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/builds/builder_agent/main.go b/lib/builds/builder_agent/main.go index f6bc8989..88641146 100644 --- a/lib/builds/builder_agent/main.go +++ b/lib/builds/builder_agent/main.go @@ -765,6 +765,17 @@ func prepareDockerfile(config *BuildConfig) (string, func(), error) { return dockerfileDir, cleanup, nil } +func baseBuildctlArgs(config *BuildConfig, dockerfileDir, outputOpts string) []string { + return []string{ + "build", + "--frontend", "dockerfile.v0", + "--local", "context=" + config.SourcePath, + "--local", "dockerfile=" + dockerfileDir, + "--output", outputOpts, + "--metadata-file", "/tmp/build-metadata.json", + } +} + func runBuild(ctx context.Context, config *BuildConfig, dockerfileDir string, logWriter io.Writer) (string, string, error) { var buildLogs bytes.Buffer @@ -794,14 +805,7 @@ func runBuild(ctx context.Context, config *BuildConfig, dockerfileDir string, lo log.Printf("Using HTTPS registry (secure mode): %s", registryHost) } - args := []string{ - "build", - "--frontend", "dockerfile.v0", - "--local", "context=" + config.SourcePath, - "--local", "dockerfile=" + dockerfileDir, - "--output", outputOpts, - "--metadata-file", "/tmp/build-metadata.json", - } + args := baseBuildctlArgs(config, dockerfileDir, outputOpts) // Two-tier cache implementation: // 1. Import from global cache (if runtime specified) - always read-only for regular builds diff --git a/lib/builds/builder_agent/main_test.go b/lib/builds/builder_agent/main_test.go index 012c6bfd..388587c9 100644 --- a/lib/builds/builder_agent/main_test.go +++ b/lib/builds/builder_agent/main_test.go @@ -3,9 +3,28 @@ package main import ( "os" "path/filepath" + "slices" "testing" ) +func TestBaseBuildctlArgsSeparatesDockerfileFromContext(t *testing.T) { + config := &BuildConfig{SourcePath: "/tmp/source"} + + got := baseBuildctlArgs(config, "/tmp/dockerfile", "type=image,name=example") + want := []string{ + "build", + "--frontend", "dockerfile.v0", + "--local", "context=/tmp/source", + "--local", "dockerfile=/tmp/dockerfile", + "--output", "type=image,name=example", + "--metadata-file", "/tmp/build-metadata.json", + } + + if !slices.Equal(got, want) { + t.Fatalf("unexpected buildctl args:\n got: %q\nwant: %q", got, want) + } +} + func TestPrepareDockerfileKeepsConfiguredDockerfileOutsideSource(t *testing.T) { sourceDir := t.TempDir() dockerfile := "FROM alpine:3.21\nCOPY . /app\n"