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 .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
-e GITHUB_TOKEN
-v "$GITHUB_WORKSPACE:/workspace"
-w /workspace
ghcr.io/goreleaser/goreleaser-cross:v1.26.4
ghcr.io/goreleaser/goreleaser-cross:v1.27.1-1
release --clean
env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }}
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: rust
version: 0.0.35
version: 0.0.36
32 changes: 9 additions & 23 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,9 @@ func (s *Builder) Build(ctx context.Context, req *builderv0.BuildRequest) (*buil
defer s.Wool.Catch()
ctx = s.Wool.Inject(ctx)

s.Infof("building rust service docker image")
if !services.BuildPlanRequested(req) {
return s.Builder.BuildError(fmt.Errorf("BuildRequest.output_directory is required for image recipes"))
}

dockerRequest, err := s.Builder.DockerBuildRequest(ctx, req)
if err != nil {
Expand All @@ -213,37 +215,21 @@ func (s *Builder) Build(ctx context.Context, req *builderv0.BuildRequest) (*buil

docker := DockerTemplating{}

_ = shared.DeleteFile(ctx, s.Location+"/builder/Dockerfile")

err = s.Templates(ctx, docker, services.WithBuilder(builderFS))
emitted, err := services.PrepareRecipeDestination(builderFS, req.GetOutputDirectory())
if err != nil {
return s.Builder.BuildError(err)
}

// When the caller owns the build (output_directory set), emit the recipe and
// let the caller run docker buildx instead of building the image in-process.
if services.BuildPlanRequested(req) {
return s.Builder.SingleImageBuildResponse(req, image.FullName())
}

b, err := dockerhelpers.NewBuilder(dockerhelpers.BuilderConfiguration{
Root: s.Location,
Dockerfile: "builder/Dockerfile",
Ignorefile: "builder/dockerignore",
Destination: image,
Output: s.Wool,
})
err = s.Templates(ctx, docker, services.WithBuilder(builderFS).WithDestination("%s", req.GetOutputDirectory()))
if err != nil {
return s.Builder.BuildError(err)
}

_, err = b.Build(ctx)
if err != nil {
return s.Builder.BuildError(err)
}
return s.Builder.SingleImageBuildResponse(req, image.FullName(), emitted)
}

s.Builder.WithDockerImages(image)
return s.Builder.BuildResponse()
func (s *Builder) BuildCapabilities(context.Context, *builderv0.BuildCapabilitiesRequest) (*builderv0.BuildCapabilitiesResponse, error) {
return &builderv0.BuildCapabilitiesResponse{BuildxSelection: true}, nil
}

func (s *Builder) Deploy(ctx context.Context, req *builderv0.DeploymentRequest) (*builderv0.DeploymentResponse, error) {
Expand Down
142 changes: 109 additions & 33 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,44 @@ package main

import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"github.com/codefly-dev/core/agents/services"
basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0"
)

// When the caller owns the build (BuildRequest.output_directory set), Build
// renders the recipe into that directory and returns a DockerBuildPlan instead
// of running docker build in-process, so the CLI builds the image multi-arch.
func TestBuildEmitsRecipeWhenOutputDirectorySet(t *testing.T) {
ctx := context.Background()
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(context.Background(), 10*time.Second)
defer cancel()

location := t.TempDir()
outputDirectory := filepath.Join(location, "builder")
outputDirectory := filepath.Join(t.TempDir(), "recipes")

protected := filepath.Join(location, "protected")
if err := os.WriteFile(protected, []byte("must stay unchanged"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(outputDirectory, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink(protected, filepath.Join(outputDirectory, "Dockerfile")); err != nil {
t.Fatal(err)
}

svc := NewService()
identity := &basev0.ServiceIdentity{
Expand All @@ -39,41 +60,96 @@ func TestBuildEmitsRecipeWhenOutputDirectorySet(t *testing.T) {
}},
}

response, err := NewBuilder(svc).Build(ctx, request)
server := grpc.NewServer()
builderv0.RegisterBuilderServer(server, NewBuilder(svc))
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("build: %v", err)
t.Fatal(err)
}
if state := response.GetState().GetState(); state != builderv0.BuildStatus_SUCCESS {
t.Fatalf("build state = %v: %s", state, response.GetState().GetMessage())
go func() { _ = server.Serve(listener) }()
t.Cleanup(server.Stop)
connection, err := grpc.NewClient(listener.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := connection.Close(); err != nil {
t.Error(err)
}
})
client := services.NewBuilderAgentClient(connection)
for _, selection := range []string{"", "explicit", "cache-selected"} {
request.GetBuildContext().GetDockerBuildContext().BuildxBuilder = selection
if selection == "cache-selected" {
request.GetBuildContext().GetDockerBuildContext().Cache = &builderv0.BuildCacheOptions{Backend: "registry", Scope: "test/app", Exports: []string{"registry.example.com/cache"}}
}
response, err := client.Build(ctx, request)
if err != nil {
t.Fatalf("build: %v", err)
}
if state := response.GetState().GetState(); state != builderv0.BuildStatus_SUCCESS {
t.Fatalf("build state = %v: %s", state, response.GetState().GetMessage())
}

plan := response.GetResult().GetDockerBuildPlan()
if plan == nil {
t.Fatalf("expected a docker build plan, got %T", response.GetResult().GetKind())
}
if plan.GetContractVersion() != services.DockerBuildRecipeContractVersion {
t.Fatalf("contract version = %q, want %q", plan.GetContractVersion(), services.DockerBuildRecipeContractVersion)
}
if len(plan.GetRecipes()) != 1 {
t.Fatalf("expected 1 recipe, got %d", len(plan.GetRecipes()))
}
recipe := plan.GetRecipes()[0]
if recipe.GetDockerfile() != "Dockerfile" || recipe.GetContext() != "." {
t.Fatalf("recipe references dockerfile %q context %q", recipe.GetDockerfile(), recipe.GetContext())
plan := response.GetResult().GetDockerBuildPlan()
if plan == nil {
t.Fatalf("expected a docker build plan, got %T", response.GetResult().GetKind())
}
if plan.GetContractVersion() != services.DockerBuildRecipeContractVersion {
t.Fatalf("contract version = %q, want %q", plan.GetContractVersion(), services.DockerBuildRecipeContractVersion)
}
if len(plan.GetRecipes()) != 1 {
t.Fatalf("expected 1 recipe, got %d", len(plan.GetRecipes()))
}
recipe := plan.GetRecipes()[0]
if recipe.GetDockerfile() != "Dockerfile" || recipe.GetContext() != "." {
t.Fatalf("recipe references dockerfile %q context %q", recipe.GetDockerfile(), recipe.GetContext())
}
if recipe.GetImage() == "" {
t.Fatalf("recipe carries no image reference")
}
if got := recipe.GetPlatforms(); len(got) != 2 || got[0] != "linux/amd64" || got[1] != "linux/arm64" {
t.Fatalf("recipe platforms = %v, want [linux/amd64 linux/arm64]", got)
}

// The plan is emitted over the on-disk recipe tree, so the caller can verify
// the tree it received against the plan's digest before building.
if err := services.VerifyDockerBuildPlan(outputDirectory, plan); err != nil {
t.Fatalf("verify plan against emitted tree: %v", err)
}
if _, err := os.Stat(filepath.Join(outputDirectory, "Dockerfile")); err != nil {
t.Fatalf("Dockerfile not rendered into output directory: %v", err)
}
}
if recipe.GetImage() == "" {
t.Fatalf("recipe carries no image reference")
untouched, err := os.ReadFile(protected)
if err != nil {
t.Fatal(err)
}
if got := recipe.GetPlatforms(); len(got) != 2 || got[0] != "linux/amd64" || got[1] != "linux/arm64" {
t.Fatalf("recipe platforms = %v, want [linux/amd64 linux/arm64]", got)
if string(untouched) != "must stay unchanged" {
t.Fatal("rendering overwrote the Dockerfile symlink target")
}

// The plan is emitted over the on-disk recipe tree, so the caller can verify
// the tree it received against the plan's digest before building.
if err := services.VerifyDockerBuildPlan(outputDirectory, plan); err != nil {
t.Fatalf("verify plan against emitted tree: %v", err)
fresh, err := os.ReadFile(filepath.Join(outputDirectory, "Dockerfile"))
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(outputDirectory, "Dockerfile")); err != nil {
t.Fatalf("Dockerfile not rendered into output directory: %v", err)
for _, destination := range []string{"", "relative"} {
request.OutputDirectory = destination
response, err := client.Build(ctx, request)
if err != nil {
t.Fatal(err)
}
if response.GetState().GetState() != builderv0.BuildStatus_ERROR || !strings.Contains(response.GetState().GetMessage(), "output_directory") {
t.Fatalf("expected destination rejection: %v", response)
}
unchanged, err := os.ReadFile(filepath.Join(outputDirectory, "Dockerfile"))
if err != nil {
t.Fatal(err)
}
if string(unchanged) != string(fresh) {
t.Fatal("rejected build modified recipe")
}
if _, err := os.Stat(filepath.Join(location, "builder")); !os.IsNotExist(err) {
t.Fatalf("rejected build prepared service builder directory: %v", err)
}
}
}
61 changes: 31 additions & 30 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ module github.com/codefly-dev/service-rust
go 1.27.0

require (
github.com/codefly-dev/core v0.3.8
google.golang.org/grpc v1.83.0
github.com/codefly-dev/core v0.3.29
google.golang.org/grpc v1.83.1
gopkg.in/yaml.v3 v3.0.1
)

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1 // indirect
buf.build/go/protovalidate v1.3.0 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2 // indirect
buf.build/go/protovalidate v1.4.0 // indirect
cel.dev/cel-go v0.32.0 // indirect
cel.dev/expr v0.25.3 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
Expand All @@ -23,15 +24,15 @@ require (
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cheggaaa/pb/v3 v3.2.0 // indirect
github.com/cheggaaa/pb/v3 v3.2.1 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.5 // indirect
github.com/codefly-dev/gortk v0.2.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dekobon/tree-sitter-groovy v0.2.2 // indirect
github.com/dekobon/tree-sitter-groovy v0.3.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.5.2+incompatible // indirect
github.com/docker/go-connections v0.8.1 // indirect
Expand All @@ -47,29 +48,28 @@ require (
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/analysis v0.26.0 // indirect
github.com/go-openapi/analysis v1.0.0 // indirect
github.com/go-openapi/errors v0.22.8 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/loads v0.25.1 // indirect
github.com/go-openapi/spec v0.22.9 // indirect
github.com/go-openapi/strfmt v0.27.0 // indirect
github.com/go-openapi/jsonpointer v1.0.1 // indirect
github.com/go-openapi/jsonreference v1.0.2 // indirect
github.com/go-openapi/loads v0.25.3 // indirect
github.com/go-openapi/spec v1.0.1 // indirect
github.com/go-openapi/strfmt v0.27.2 // indirect
github.com/go-openapi/swag v0.28.0 // indirect
github.com/go-openapi/swag/cmdutils v0.28.0 // indirect
github.com/go-openapi/swag/conv v0.28.0 // indirect
github.com/go-openapi/swag/conv v0.29.1 // indirect
github.com/go-openapi/swag/fileutils v0.28.0 // indirect
github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
github.com/go-openapi/swag/loading v0.28.0 // indirect
github.com/go-openapi/swag/mangling v0.28.0 // indirect
github.com/go-openapi/swag/jsonutils v0.29.1 // indirect
github.com/go-openapi/swag/loading v0.29.1 // indirect
github.com/go-openapi/swag/mangling v0.29.1 // indirect
github.com/go-openapi/swag/netutils v0.28.0 // indirect
github.com/go-openapi/swag/pools v0.28.0 // indirect
github.com/go-openapi/swag/stringutils v0.28.0 // indirect
github.com/go-openapi/swag/typeutils v0.28.0 // indirect
github.com/go-openapi/swag/yamlutils v0.28.0 // indirect
github.com/go-openapi/swag/pools v0.29.1 // indirect
github.com/go-openapi/swag/stringutils v0.29.1 // indirect
github.com/go-openapi/swag/typeutils v0.29.1 // indirect
github.com/go-openapi/swag/yamlutils v0.29.1 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gofrs/flock v0.13.1 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/cel-go v0.31.0 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-github/v89 v89.0.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
Expand All @@ -86,6 +86,7 @@ require (
github.com/mattn/go-runewidth v0.0.28 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/oklog/ulid/v2 v2.1.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand Down Expand Up @@ -113,25 +114,25 @@ require (
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
github.com/yuin/goldmark v1.8.6 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.70.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.70.0 // indirect
go.opentelemetry.io/otel v1.45.0 // indirect
go.opentelemetry.io/otel/metric v1.45.0 // indirect
go.opentelemetry.io/otel/trace v1.45.0 // indirect
go.opentelemetry.io/otel v1.46.0 // indirect
go.opentelemetry.io/otel/metric v1.46.0 // indirect
go.opentelemetry.io/otel/trace v1.46.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
golang.org/x/exp v0.0.0-20260820142414-ca536658362e // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.41.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260817212433-ac3dfec99bb1 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260817212433-ac3dfec99bb1 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
Expand Down
Loading
Loading