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
16 changes: 16 additions & 0 deletions api/v1/container_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ type ContainerBuildContext struct {
// +listType=map
// +listMapKey=key
Labels []ContainerLabel `json:"labels,omitempty"`

// Optional target platform for the build (e.g. "linux/amd64")
Platform string `json:"platform,omitempty"`
}

// +k8s:openapi-gen=true
Expand Down Expand Up @@ -858,6 +861,15 @@ func (cs *ContainerSpec) GetLifecycleKey() (string, bool, error) {
_, writeErr = fnvHash.Write([]byte(cs.Build.Stage))
hashErr = errors.Join(hashErr, writeErr)

// Add the build platform to the hash; changing the target platform
// produces a different image, so persistent containers must rebuild.
// Encoded via gob (length-framed) and only when set, to keep the
// hash unambiguous against the adjacent Stage write and to preserve
// the legacy key for existing workloads where Platform is unset.
if cs.Build.Platform != "" {
hashErr = errors.Join(hashErr, encoder.Encode(cs.Build.Platform))
}

if len(cs.Build.Labels) > 0 {
// Add the build labels to the hash
sortedLabels := slices.Clone(cs.Build.Labels)
Expand Down Expand Up @@ -1421,6 +1433,10 @@ func (c1 *ContainerBuildContext) Equal(c2 *ContainerBuildContext) bool {
return false
}

if c1.Platform != c2.Platform {
return false
}

// If the build arguments aren't the same
if !slices.Equal(c1.Args, c2.Args) {
return false
Expand Down
112 changes: 112 additions & 0 deletions api/v1/container_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,115 @@ func TestGetLifecycleKeyIncludesImageLayers(t *testing.T) {
assert.NotEqual(t, keyNoLayers, keyWithLayers, "lifecycle key should differ when layers are added")
assert.NotEqual(t, keyWithLayers, keyDifferentLayers, "lifecycle key should differ when layer digests differ")
}

func TestContainerBuildContextEqual(t *testing.T) {
t.Parallel()

build1 := ContainerBuildContext{
Context: "/path/to/context",
Dockerfile: "Dockerfile",
Stage: "runner",
Platform: "linux/amd64",
}

build2 := ContainerBuildContext{
Context: "/path/to/context",
Dockerfile: "Dockerfile",
Stage: "runner",
Platform: "linux/amd64",
}

buildDifferentPlatform := ContainerBuildContext{
Context: "/path/to/context",
Dockerfile: "Dockerfile",
Stage: "runner",
Platform: "linux/arm64",
}

buildNoPlatform := ContainerBuildContext{
Context: "/path/to/context",
Dockerfile: "Dockerfile",
Stage: "runner",
}

assert.True(t, build1.Equal(&build2), "identical build contexts should be equal")
assert.False(t, build1.Equal(&buildDifferentPlatform), "build contexts with different platforms should not be equal")
assert.False(t, build1.Equal(&buildNoPlatform), "build context with platform should not equal one without")
assert.True(t, build1.Equal(&build1), "build context should be equal to itself")
assert.False(t, build1.Equal(nil), "build context should not be equal to nil")

var nilBuild *ContainerBuildContext
assert.True(t, nilBuild.Equal(nil), "two nil build contexts should be equal")
assert.False(t, nilBuild.Equal(&build1), "nil build context should not be equal to non-nil")
}

func TestGetLifecycleKeyIncludesBuildPlatform(t *testing.T) {
t.Parallel()

specNoPlatform := ContainerSpec{
Build: &ContainerBuildContext{
Context: "/nonexistent/context",
Dockerfile: "Dockerfile",
},
}

specAmd64 := ContainerSpec{
Build: &ContainerBuildContext{
Context: "/nonexistent/context",
Dockerfile: "Dockerfile",
Platform: "linux/amd64",
},
}

specArm64 := ContainerSpec{
Build: &ContainerBuildContext{
Context: "/nonexistent/context",
Dockerfile: "Dockerfile",
Platform: "linux/arm64",
},
}

keyNoPlatform, _, errNoPlatform := specNoPlatform.GetLifecycleKey()
require.NoError(t, errNoPlatform)

keyAmd64, _, errAmd64 := specAmd64.GetLifecycleKey()
require.NoError(t, errAmd64)

keyArm64, _, errArm64 := specArm64.GetLifecycleKey()
require.NoError(t, errArm64)

assert.NotEqual(t, keyNoPlatform, keyAmd64, "lifecycle key should differ when platform is added")
assert.NotEqual(t, keyAmd64, keyArm64, "lifecycle key should differ between platforms")
}

func TestGetLifecycleKeyDisambiguatesStageAndPlatform(t *testing.T) {
t.Parallel()

// Without length framing, (Stage="ab", Platform="c") and
// (Stage="a", Platform="bc") would hash identically.
specA := ContainerSpec{
Build: &ContainerBuildContext{
Context: "/nonexistent/context",
Dockerfile: "Dockerfile",
Stage: "ab",
Platform: "c",
},
}

specB := ContainerSpec{
Build: &ContainerBuildContext{
Context: "/nonexistent/context",
Dockerfile: "Dockerfile",
Stage: "a",
Platform: "bc",
},
}

keyA, _, errA := specA.GetLifecycleKey()
require.NoError(t, errA)

keyB, _, errB := specB.GetLifecycleKey()
require.NoError(t, errB)

assert.NotEqual(t, keyA, keyB, "stage/platform boundary must be unambiguous")
}
5 changes: 5 additions & 0 deletions internal/docker/cli_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,11 @@ func (dco *DockerCliOrchestrator) BuildImage(ctx context.Context, options contai
args = append(args, "--label", fmt.Sprintf("%s=%s", label.Key, label.Value))
}

// If a target platform is specified, build for that platform
if options.Platform != "" {
args = append(args, "--platform", options.Platform)
}

// Enable plain output mode (Docker only)
args = append(args, "--progress", "plain")

Expand Down
5 changes: 5 additions & 0 deletions internal/podman/cli_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,11 @@ func (pco *PodmanCliOrchestrator) BuildImage(ctx context.Context, options contai
args = append(args, "--label", fmt.Sprintf("%s=%s", label.Key, label.Value))
}

// If a target platform is specified, build for that platform
if options.Platform != "" {
args = append(args, "--platform", options.Platform)
}

// Append the build context argument
args = append(args, options.Context)

Expand Down
7 changes: 7 additions & 0 deletions pkg/generated/openapi/zz_generated.openapi.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading