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
3 changes: 3 additions & 0 deletions agents/services/base_builder.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -979,6 +979,9 @@ func (s *BuilderWrapper) LogDeployRequest(req *builderv0.DeploymentRequest, log
}

func (s *BuilderWrapper) DockerBuildRequest(_ context.Context, req *builderv0.BuildRequest) (*builderv0.DockerBuildContext, error) {
if err := ValidateBuildRequestOutputDirectory(req); err != nil {
return nil, s.Wool.Wrapf(err, "cannot build")
}
switch v := req.BuildContext.Kind.(type) {
case *builderv0.BuildContext_DockerBuildContext:
return v.DockerBuildContext, nil
Expand Down
23 changes: 23 additions & 0 deletions agents/services/docker_recipe.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,29 @@ import (
// validates before building from an emitted plan.
const DockerBuildRecipeContractVersion = "codefly.dev/docker-build-recipe/v1"

// ValidateBuildRequestOutputDirectory enforces the BuildRequest.output_directory
// contract: when set, the destination must be an absolute path the caller owns.
// Empty is valid and selects the legacy in-agent build. A relative path is
// rejected with a clear error rather than silently resolved against whatever
// working directory the agent happens to run in — the agent and the caller would
// otherwise resolve it against different directories and the recipe handshake
// would break with no error at all. DockerBuildRequest calls this so every agent
// build enforces the invariant at the boundary where the request enters core;
// the CLI resolves its destination to absolute and can call it before sending.
// BuildDockerBuildPlan does not enforce it — it takes a bare destination and its
// tree walk is relative-safe, so a guard there would reject valid callers of a
// generic helper rather than catch the contract violation at the request boundary.
func ValidateBuildRequestOutputDirectory(req *builderv0.BuildRequest) error {
dir := req.GetOutputDirectory()
if dir == "" {
return nil
}
if !filepath.IsAbs(dir) {
return fmt.Errorf("BuildRequest.output_directory must be absolute, got %q", dir)
}
return nil
}

// BuildDockerBuildPlan inventories the recipe tree an agent wrote to destination
// and returns a build plan: the ordered recipes plus the canonical sorted file
// inventory with per-file sha256 digests and an aggregate digest that is a
Expand Down
49 changes: 49 additions & 0 deletions agents/services/docker_recipe_test.go
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
package services

import (
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"testing"

builderv0 "github.com/codefly-dev/core/generated/go/codefly/services/builder/v0"
"github.com/codefly-dev/core/wool"
"github.com/stretchr/testify/require"
)

Expand DownExpand Up@@ -61,6 +63,53 @@ func TestBuildDockerBuildPlanInventoriesRecipeTree(t *testing.T) {
require.Error(t, VerifyDockerBuildPlan(destination, plan))
}

func TestValidateBuildRequestOutputDirectory(t *testing.T) {
// A nil request carries no directory and selects the legacy in-agent build.
require.NoError(t, ValidateBuildRequestOutputDirectory(nil))

// Empty selects the legacy in-agent build and is valid.
require.NoError(t, ValidateBuildRequestOutputDirectory(&builderv0.BuildRequest{}))

// An absolute destination honors the contract.
require.NoError(t, ValidateBuildRequestOutputDirectory(&builderv0.BuildRequest{
OutputDirectory: filepath.Join(t.TempDir(), "recipes"),
}))

// A relative destination is rejected at the boundary that populates the field.
err := ValidateBuildRequestOutputDirectory(&builderv0.BuildRequest{OutputDirectory: "recipes/out"})
require.Error(t, err)
require.Contains(t, err.Error(), "absolute")
}

func TestDockerBuildRequestEnforcesAbsoluteOutputDirectory(t *testing.T) {
wrapper := &BuilderWrapper{Base: &Base{Wool: wool.Get(context.Background())}}
dockerContext := &builderv0.BuildContext{
Kind: &builderv0.BuildContext_DockerBuildContext{DockerBuildContext: &builderv0.DockerBuildContext{}},
}

// A relative output_directory is rejected before the build proceeds, so the
// agent never writes recipes where the caller cannot find them.
_, err := wrapper.DockerBuildRequest(context.Background(), &builderv0.BuildRequest{
BuildContext: dockerContext,
OutputDirectory: "recipes/out",
})
require.Error(t, err)
require.Contains(t, err.Error(), "absolute")

// An absolute output_directory passes through to the docker build context.
got, err := wrapper.DockerBuildRequest(context.Background(), &builderv0.BuildRequest{
BuildContext: dockerContext,
OutputDirectory: filepath.Join(t.TempDir(), "recipes"),
})
require.NoError(t, err)
require.NotNil(t, got)

// An empty output_directory (legacy in-agent build) passes through.
got, err = wrapper.DockerBuildRequest(context.Background(), &builderv0.BuildRequest{BuildContext: dockerContext})
require.NoError(t, err)
require.NotNil(t, got)
}

func TestBuildDockerBuildPlanDigestChangesWithContent(t *testing.T) {
destination := t.TempDir()
dockerfile := filepath.Join(destination, "Dockerfile")
Expand Down
Loading