From 74e9178826141e6f2406218d563a58dbd7f8e313 Mon Sep 17 00:00:00 2001 From: Antoine Toussaint Date: Sat, 12 Sep 2026 12:46:31 -0400 Subject: [PATCH] fix: reject in-agent Go image execution before preparation (#60) Delete the shared-runner fallback and the legacy Docker execution test. Exercise the production gRPC handler with Docker/Buildx absent, pin the corrected Core commit, and prepare Go 0.0.48 because 0.0.47 is already published. --- agent.codefly.yaml | 2 +- go.mod | 2 +- go.sum | 4 +- pkg/builder/builder.go | 13 ++---- recipe_test.go | 89 +++++++++++++++++++++--------------------- 5 files changed, 52 insertions(+), 58 deletions(-) diff --git a/agent.codefly.yaml b/agent.codefly.yaml index eebbd50..179283c 100644 --- a/agent.codefly.yaml +++ b/agent.codefly.yaml @@ -1,4 +1,4 @@ publisher: codefly.dev kind: codefly:service name: go -version: 0.0.47 +version: 0.0.48 diff --git a/go.mod b/go.mod index c694d17..63576b4 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/codefly-dev/service-go go 1.27.0 require ( - github.com/codefly-dev/core v0.3.27 + github.com/codefly-dev/core v0.3.28-0.20260912122621-177cb87e85ee golang.org/x/tools v0.49.0 google.golang.org/grpc v1.83.0 google.golang.org/protobuf v1.36.12 diff --git a/go.sum b/go.sum index 2bae4ee..3babbf2 100644 --- a/go.sum +++ b/go.sum @@ -41,8 +41,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.5 h1:O64F26HEqNhznd/hrC5KZXVKYuKM2rx4deZDTc4ihQA= github.com/cloudflare/circl v1.6.5/go.mod h1:h5LNyxAc5nTue9DS5jT+48en2PSDYt3zdGnz5OstK6c= -github.com/codefly-dev/core v0.3.27 h1:SI0EhUvGr+WlIkuLEuTDQuMjZX5ThgmPGJO2f0NPyzM= -github.com/codefly-dev/core v0.3.27/go.mod h1:Z9//zS5LnN1MdX5Oi9AuPkXOHYSfzi25GyPi0JU044w= +github.com/codefly-dev/core v0.3.28-0.20260912122621-177cb87e85ee h1:s4VeLT3Y7ypZN8VeKyb2JWdlePDkqFSqyfVbnkARUP0= +github.com/codefly-dev/core v0.3.28-0.20260912122621-177cb87e85ee/go.mod h1:Z9//zS5LnN1MdX5Oi9AuPkXOHYSfzi25GyPi0JU044w= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index 4786ae8..ff0d846 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -128,19 +128,14 @@ func (s *Builder) BuildCapabilities(context.Context, *builderv0.BuildCapabilitie return &builderv0.BuildCapabilitiesResponse{BuildxSelection: true}, nil } -// Build produces a Docker image. When the CLI supplies an output directory it -// owns the docker build: the agent renders the recipe (Dockerfile + context) -// into that directory and returns a DockerBuildPlan the CLI builds multi-arch -// with buildx. With no output directory the agent builds the image in-process -// via the shared go builder helper. +// Build emits a recipe for the CLI to build and publish. func (s *Builder) Build(ctx context.Context, req *builderv0.BuildRequest) (*builderv0.BuildResponse, error) { defer s.Wool.Catch() ctx = s.Wool.Inject(ctx) - if out := req.GetOutputDirectory(); out != "" { - return s.buildRecipe(ctx, req, out) + if req.GetOutputDirectory() == "" { + return s.Builder.BuildError(fmt.Errorf("BuildRequest.output_directory is required for image recipes")) } - return golanghelpers.BuildGoDocker(ctx, s.Base.Builder, req, s.Location, - s.cfg.Requirements, s.cfg.BuilderFS, s.cfg.GoVersion, s.cfg.AlpineVersion) + return s.buildRecipe(ctx, req, req.GetOutputDirectory()) } // buildRecipe renders the Dockerfile, dockerignore, and Go source into the diff --git a/recipe_test.go b/recipe_test.go index eecda36..5cbc80b 100644 --- a/recipe_test.go +++ b/recipe_test.go @@ -238,6 +238,12 @@ func TestBuildxRecipeOverGRPC(t *testing.T) { for _, cached := range []bool{false, true} { t.Run(fmt.Sprintf("cache=%t", cached), func(t *testing.T) { b, _ := loadedBuilder(t) + t.Setenv("PATH", t.TempDir()) + for _, executable := range []string{"docker", "buildx"} { + if _, err := exec.LookPath(executable); err == nil { + t.Fatalf("%s unexpectedly on PATH", executable) + } + } ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() out := filepath.Join(t.TempDir(), "recipe") @@ -290,51 +296,44 @@ func TestBuildxRecipeOverGRPC(t *testing.T) { } } -func TestLegacyBuildxOverGRPC(t *testing.T) { - if os.Getenv("CODEFLY_TEST_BUILDX") != "1" { - t.Skip("set CODEFLY_TEST_BUILDX=1 to build with Docker") - } - ctx, cancel := context.WithTimeout(t.Context(), 4*time.Minute) - defer cancel() - run := func(ctx context.Context, args ...string) string { - t.Helper() - output, err := exec.CommandContext(ctx, "docker", args...).CombinedOutput() - if err != nil { - t.Fatalf("docker %v: %v\n%s", args, err, output) +func TestBuildRejectsMissingOrRelativeDestinationOverGRPC(t *testing.T) { + for _, selection := range []string{"", "explicit", "cache-selected"} { + for _, destination := range []string{"", "relative"} { + t.Run(fmt.Sprintf("builder=%s/destination=%s", selection, destination), func(t *testing.T) { + b, _ := loadedBuilder(t) + t.Setenv("PATH", t.TempDir()) + original := filepath.Join(b.Location, "builder", "Dockerfile") + write(t, original, "original recipe") + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) + defer cancel() + docker := &builderv0.DockerBuildContext{DockerRepository: "registry.example.com", BuildxBuilder: selection} + if selection == "cache-selected" { + docker.Cache = &builderv0.BuildCacheOptions{Backend: "registry", Scope: "test/myservice", Exports: []string{"registry.example.com/cache"}} + } + request := &builderv0.BuildRequest{OutputDirectory: destination, BuildContext: &builderv0.BuildContext{Kind: &builderv0.BuildContext_DockerBuildContext{DockerBuildContext: docker}}} + response, err := grpcBuilderClient(t, b).Build(ctx, request) + if err != nil { + if !strings.Contains(err.Error(), "output_directory") { + t.Fatalf("unexpected error: %v", err) + } + } else if response.GetState().GetState() != builderv0.BuildStatus_ERROR || !strings.Contains(response.GetState().GetMessage(), "output_directory") { + t.Fatalf("expected destination rejection, got %v", response) + } + contents, err := os.ReadFile(original) + if err != nil { + t.Fatal(err) + } + if string(contents) != "original recipe" { + t.Fatal("rejected request overwrote existing recipe") + } + entries, err := os.ReadDir(filepath.Dir(original)) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 { + t.Fatal("rejected request prepared builder files") + } + }) } - return strings.TrimSpace(string(output)) - } - name := fmt.Sprintf("codefly-go-test-%d", time.Now().UnixNano()) - run(ctx, "buildx", "create", "--name", name, "--driver", "docker-container", "--bootstrap") - t.Cleanup(func() { - cleanup, stop := context.WithTimeout(context.Background(), time.Minute) - defer stop() - run(cleanup, "buildx", "rm", name) - }) - t.Setenv("BUILDX_BUILDER", "must-not-use-ambient-builder") - t.Setenv("CODEFLY_BUILD_PLATFORM", "linux/amd64") - b, _ := loadedBuilder(t) - client := grpcBuilderClient(t, b) - response, err := client.Build(ctx, &builderv0.BuildRequest{BuildContext: &builderv0.BuildContext{Kind: &builderv0.BuildContext_DockerBuildContext{DockerBuildContext: &builderv0.DockerBuildContext{DockerRepository: name, BuildxBuilder: name}}}}) - if err != nil { - t.Fatal(err) - } - if response.GetState().GetState() != builderv0.BuildStatus_SUCCESS { - t.Fatalf("Build failed: %v", response.GetState()) - } - if response.GetResult().GetDockerBuildPlan() != nil { - t.Fatal("legacy build returned a recipe") - } - if response.GetBuildxBuilder() != name { - t.Fatalf("builder acknowledgement = %q, want %q", response.GetBuildxBuilder(), name) - } - image := b.Base.DockerImage(&builderv0.DockerBuildContext{DockerRepository: name}).FullName() - t.Cleanup(func() { - cleanup, stop := context.WithTimeout(context.Background(), time.Minute) - defer stop() - run(cleanup, "image", "rm", image) - }) - if got := run(ctx, "image", "inspect", "--format", "{{.Architecture}}", image); got != "amd64" { - t.Fatalf("image architecture = %q, want amd64", got) } }