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
2 changes: 1 addition & 1 deletion agent.codefly.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
publisher: codefly.dev
kind: codefly:service
name: go
version: 0.0.47
version: 0.0.48
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
13 changes: 4 additions & 9 deletions pkg/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
89 changes: 44 additions & 45 deletions recipe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
}
Loading