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
4 changes: 2 additions & 2 deletions .github/workflows/build-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.27.2"
go-version: "1.27.3"
- name: Verify Buildx selection and cache arguments reach the docker CLI
if: matrix.language == 'go'
run: go test ./agents/helpers/docker -run '^TestBuildx|^TestBuildOmits|^TestBuildCarries' -count=1 -v -timeout=5m
Expand Down Expand Up @@ -62,7 +62,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.27.2"
go-version: "1.27.3"
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: registry-cache-${{ matrix.language }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
- name: set up
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: "1.27.2"
go-version: "1.27.3"

- name: checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
8 changes: 7 additions & 1 deletion agents/services/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,13 @@ func (b *BuilderAgent) Build(ctx context.Context, req *builderv0.BuildRequest, o
if err == nil && cache != nil && resp.GetState().GetState() == builderv0.BuildStatus_SUCCESS && resp.GetResult().GetDockerBuildPlan() == nil && resp.GetCacheContractVersion() != dockerhelpers.CacheContractVersion {
return nil, fmt.Errorf("builder agent did not acknowledge build cache contract %s; upgrade the agent or remove cache options", dockerhelpers.CacheContractVersion)
}
if selected != "" && err == nil && resp.GetState().GetState() == builderv0.BuildStatus_SUCCESS && resp.GetResult().GetDockerBuildPlan() == nil && resp.GetBuildxBuilder() != selected {
// Only an in-agent image build has an executor to acknowledge. A recipe
// (docker_build_plan) is built by the caller, on the builder it selected;
// an answer with no result kind built nothing — a vendor-image agent whose
// image is published elsewhere — and has nothing to acknowledge either.
// Reading "no recipe" as "built in-agent" refused every render composing
// such an agent the moment the environment selected a builder.
if selected != "" && err == nil && resp.GetState().GetState() == builderv0.BuildStatus_SUCCESS && resp.GetResult().GetDockerBuildResult() != nil && resp.GetBuildxBuilder() != selected {
return nil, fmt.Errorf("builder agent did not acknowledge requested Buildx builder %q; upgrade the agent", selected)
}
if err == nil && req.GetExecution() != nil {
Expand Down
20 changes: 14 additions & 6 deletions agents/services/builder_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package services

import (
"context"
"fmt"
"net"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -87,12 +86,21 @@ func (s *buildxTestServer) Build(ctx context.Context, req *builderv0.BuildReques
return resp, err
}

// Only an in-agent build has an executor to acknowledge: a recipe is built by
// the caller on the builder it selected, and an answer that built nothing (a
// vendor-image agent) has nothing to acknowledge. Treating "no recipe" as
// "built in-agent" refused every render composing a vendor-image agent.
func TestBuildxSelectionRequiresExecutorAcknowledgementOverGRPC(t *testing.T) {
for _, recipe := range []bool{false, true} {
t.Run(fmt.Sprintf("recipe=%t", recipe), func(t *testing.T) {
for name, result := range map[string]*builderv0.BuildResult{
"in-agent build": {Kind: &builderv0.BuildResult_DockerBuildResult{DockerBuildResult: &builderv0.DockerBuildResult{}}},
"recipe": {Kind: &builderv0.BuildResult_DockerBuildPlan{DockerBuildPlan: &builderv0.DockerBuildPlan{}}},
"nothing built": nil,
} {
t.Run(name, func(t *testing.T) {
recipe := name != "in-agent build"
wrapper := &BuilderWrapper{Base: &Base{loaded: true}}
if recipe {
wrapper.BuildResult = &builderv0.BuildResult{Kind: &builderv0.BuildResult_DockerBuildPlan{DockerBuildPlan: &builderv0.DockerBuildPlan{}}}
if result != nil {
wrapper.BuildResult = result
}
server := grpc.NewServer()
builderv0.RegisterBuilderServer(server, &buildxTestServer{
Expand Down Expand Up @@ -144,7 +152,7 @@ func TestBuildxCapabilitiesCheckedBeforeBuildOverGRPC(t *testing.T) {
} {
t.Run(tc.name, func(t *testing.T) {
implementation := &buildxTestServer{
DefaultBuilder: NewDefaultBuilder(&BuilderWrapper{Base: &Base{loaded: true}}),
DefaultBuilder: NewDefaultBuilder(&BuilderWrapper{Base: &Base{loaded: true}, BuildResult: &builderv0.BuildResult{Kind: &builderv0.BuildResult_DockerBuildResult{DockerBuildResult: &builderv0.DockerBuildResult{}}}}), // an in-agent build: the one answer that owes an acknowledgement
capabilities: &builderv0.BuildCapabilitiesResponse{BuildxSelection: tc.supported},
capabilityError: tc.probeError,
acknowledgement: tc.ack,
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/codefly-dev/core

go 1.27.0
go 1.27.3

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260825204119-511051f7f437.2
Expand Down
15 changes: 13 additions & 2 deletions runners/sandbox/bwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
// bwrap \
// --die-with-parent \
// --new-session \
// --unshare-pid \
// --proc /proc \
// --dev /dev \
// --tmpfs /tmp \
Expand All @@ -24,9 +25,18 @@ import (
// [--unshare-net]
// -- <originalCmd> <originalArgs...>
//
// `--die-with-parent` ensures the bwrap child gets SIGTERM if the Go
// `--die-with-parent` asks for the bwrap child to be killed if the Go
// parent dies abnormally. `--new-session` blocks TIOCSTI escapes from
// the sandboxed process onto the controlling tty. The system-image
// the sandboxed process onto the controlling tty. `--unshare-pid` is
// what actually bounds the payload's lifetime: with it the payload runs
// under a pid-namespace init bubblewrap supervises, so ending the
// supervisor ends the namespace and every process in it. Without it the
// bound rests on the parent-death signal alone, and that signal is not
// delivered on every platform: on Ubuntu 26.04 (kernel 7.0, bubblewrap
// 0.11.1 confined as bwrap//&unpriv_bwrap) a payload outlived its
// terminated supervisor, in its own session because of --new-session,
// holding the supervisor's stdio pipes, so the caller's reaper never
// returned and cleanup ran into its deadline. The system-image
// binds (/usr, /lib, etc.) are required because most binaries are
// dynamically linked to libraries rooted there; without them, even
// `cat` fails to start in the sandbox.
Expand Down Expand Up @@ -108,6 +118,7 @@ func (s *bwrapSandbox) buildArgs() []string {
args := []string{
"--die-with-parent",
"--new-session",
"--unshare-pid",
"--proc", "/proc",
"--dev", "/dev",
"--tmpfs", "/tmp",
Expand Down
15 changes: 15 additions & 0 deletions runners/sandbox/sandbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,21 @@ func TestBwrap_NetworkLoopback_UsesIsolatedNetwork(t *testing.T) {
require.Equal(t, []string{"/tmp/codefly-agent", "--fixture"}, cmd.Args[len(cmd.Args)-2:])
}

func TestBwrapPayloadLifetimeIsBoundToAPidNamespace(t *testing.T) {
if runtime.GOOS != "linux" {
// bubblewrap is Linux-only; returning early per the no-skip rule.
return
}
sb, err := sandbox.New()
require.NoError(t, err)
cmd := exec.Command("/tmp/codefly-agent", "--fixture")
require.NoError(t, sb.Wrap(cmd))
args := strings.Join(cmd.Args, " ")
require.Contains(t, args, "--unshare-pid",
"the payload must run under a pid-namespace init bubblewrap supervises: --die-with-parent alone is not honoured on every platform (Ubuntu 26.04 let a payload outlive its terminated supervisor)")
require.Contains(t, args, "--die-with-parent")
}

func TestSandbox_Wrap_RefusesDoubleWrap(t *testing.T) {
sb, err := sandbox.New()
if err != nil {
Expand Down
Loading