Skip to content

Commit

Permalink
azlinux/mariner: Use provided platform for outputs
Browse files Browse the repository at this point in the history
Before this change, the mariner/azl targets were ignoring the client
provided platform for the produced aritifacts.

With this change the platform is set on images so it will rely on
binfmt_misc to execute those images correctly.
There's probably some optimizations that can be made here to run certain
tasks on the native platform, some thoughts:

- Use native (host-arch) golang to download go modules
- Use native (host-arch) tdnf to download/install non-native packages
  onto the target build environment.

This does *not* add support for cross compilation (run x86 code to
generate, e.g., arm64 code).

Signed-off-by: Brian Goff <[email protected]>
  • Loading branch information
cpuguy83 committed Aug 28, 2024
1 parent cb95d4d commit a05e530
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 18 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ on:
- go.mod
- go.sum

env:
# Used in tests to determine if certain tests should be skipped.
# Setting this ensures that they are *not* skipped and instead make sure CI
# is setup to be able to properly run all tests.
DALEC_CI: "1"

permissions:
contents: read

Expand Down Expand Up @@ -93,6 +99,8 @@ jobs:
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: download deps
run: go mod download
- name: Setup QEMU
run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
- name: Run integaration tests
run: go test -v -json ./test | go run ./cmd/test2json2gha
- name: dump logs
Expand Down
4 changes: 2 additions & 2 deletions frontend/azlinux/handle_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func handleContainer(w worker) gwclient.BuildFunc {

pg := dalec.ProgressGroup("Building " + targetKey + " container: " + spec.Name)

rpmDir, err := specToRpmLLB(ctx, w, client, spec, sOpt, targetKey, pg)
rpmDir, err := specToRpmLLB(ctx, w, client, spec, sOpt, targetKey, pg, dalec.WithPlatform(platform))
if err != nil {
return nil, nil, fmt.Errorf("error creating rpm: %w", err)
}
Expand All @@ -35,7 +35,7 @@ func handleContainer(w worker) gwclient.BuildFunc {
return nil, nil, err
}

st, err := specToContainerLLB(w, spec, targetKey, rpmDir, rpms, sOpt, pg)
st, err := specToContainerLLB(w, spec, targetKey, rpmDir, rpms, sOpt, pg, dalec.WithPlatform(platform))
if err != nil {
return nil, nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/azlinux/handle_depsonly.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func handleDepsOnly(w worker) gwclient.BuildFunc {
return nil, nil, err
}

baseImg, err := w.Base(sOpt, pg)
baseImg, err := w.Base(sOpt, pg, dalec.WithPlatform(platform))
if err != nil {
return nil, nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/azlinux/handle_rpm.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func handleRPM(w worker) gwclient.BuildFunc {
return nil, nil, err
}

st, err := specToRpmLLB(ctx, w, client, spec, sOpt, targetKey, pg)
st, err := specToRpmLLB(ctx, w, client, spec, sOpt, targetKey, pg, dalec.WithPlatform(platform))
if err != nil {
return nil, nil, err
}
Expand Down
1 change: 0 additions & 1 deletion frontend/debug/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,5 @@ func Handle(ctx context.Context, client gwclient.Client) (*gwclient.Result, erro
Name: "gomods",
Description: "Outputs all the gomodule dependencies for the spec",
})

return r.Handle(ctx, client)
}
8 changes: 5 additions & 3 deletions frontend/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import (
)

const (
requestIDKey = "requestid"
dalecSubrequstForwardBuild = "dalec.forward.build"
// KeyRequestID is a key used in buildkit to performa subrequest
// This is exposed for convenience only.
KeyRequestID = "requestid"

gatewayFrontend = "gateway.v0"
dalecSubrequstForwardBuild = "dalec.forward.build"
gatewayFrontend = "gateway.v0"
)

func getDockerfile(ctx context.Context, client gwclient.Client, build *dalec.SourceBuild, defPb *pb.Definition) ([]byte, error) {
Expand Down
8 changes: 4 additions & 4 deletions frontend/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (m *BuildMux) describe() (*gwclient.Result, error) {
}

func (m *BuildMux) handleSubrequest(ctx context.Context, client gwclient.Client, opts map[string]string) (*gwclient.Result, bool, error) {
switch opts[requestIDKey] {
switch opts[KeyRequestID] {
case "":
return nil, false, nil
case subrequests.RequestSubrequestsDescribe:
Expand All @@ -135,7 +135,7 @@ func (m *BuildMux) handleSubrequest(ctx context.Context, client gwclient.Client,
res, err := handleDefaultPlatform()
return res, true, err
default:
return nil, false, errors.Errorf("unsupported subrequest %q", opts[requestIDKey])
return nil, false, errors.Errorf("unsupported subrequest %q", opts[KeyRequestID])
}
}

Expand Down Expand Up @@ -369,7 +369,7 @@ func (m *BuildMux) Handle(ctx context.Context, client gwclient.Client) (_ *gwcli
WithFields(logrus.Fields{
"handlers": maps.Keys(m.handlers),
"target": opts[keyTarget],
"requestid": opts[requestIDKey],
"requestid": opts[KeyRequestID],
"targetKey": GetTargetKey(client),
}))

Expand Down Expand Up @@ -403,7 +403,7 @@ func (m *BuildMux) Handle(ctx context.Context, client gwclient.Client) (_ *gwcli

// If this request was a request to list targets, we need to modify the response a bit
// Otherwise we can just return the result as is.
if opts[requestIDKey] == bktargets.SubrequestsTargetsDefinition.Name {
if opts[KeyRequestID] == bktargets.SubrequestsTargetsDefinition.Name {
return m.fixupListResult(matched, res)
}
return res, nil
Expand Down
10 changes: 10 additions & 0 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/identity"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

var disableDiffMerge atomic.Bool
Expand Down Expand Up @@ -399,3 +400,12 @@ func SortedMapValues[T any](m map[string]T) []T {

return out
}

// WithPlatform sets the platform in the constraints opts
// This is similar to [llb.Platform] except this takes a pointer so you don't
// need to worry about dereferencing a potentially nil pointer.
func WithPlatform(p *ocispecs.Platform) llb.ConstraintsOpt {
return constraintsOptFunc(func(c *llb.Constraints) {
c.Platform = p
})
}
144 changes: 142 additions & 2 deletions test/azlinux_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package test

import (
"bytes"
"context"
"debug/elf"
"errors"
"fmt"
"os"
Expand All @@ -10,9 +12,16 @@ import (

"github.com/Azure/dalec"
"github.com/Azure/dalec/frontend/azlinux"
"github.com/containerd/platforms"
"github.com/moby/buildkit/client/llb"
gwclient "github.com/moby/buildkit/frontend/gateway/client"
moby_buildkit_v1_frontend "github.com/moby/buildkit/frontend/gateway/pb"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

var (
linuxAmd64 = ocispecs.Platform{OS: "linux", Architecture: "amd64"}
linuxArm64 = ocispecs.Platform{OS: "linux", Architecture: "arm64"}
)

func TestMariner2(t *testing.T) {
Expand Down Expand Up @@ -41,6 +50,7 @@ func TestMariner2(t *testing.T) {
ID: "mariner",
VersionID: "2.0",
},
SupportedPlatforms: platforms.Any(linuxAmd64, linuxArm64),
})
}

Expand Down Expand Up @@ -70,6 +80,7 @@ func TestAzlinux3(t *testing.T) {
ID: "azurelinux",
VersionID: "3.0",
},
SupportedPlatforms: platforms.Any(linuxAmd64, linuxArm64),
})
}

Expand Down Expand Up @@ -124,8 +135,9 @@ type testLinuxConfig struct {
Units string
Targets string
}
Worker workerConfig
Release OSRelease
Worker workerConfig
Release OSRelease
SupportedPlatforms platforms.Matcher
}

type OSRelease struct {
Expand Down Expand Up @@ -603,6 +615,7 @@ WantedBy=multi-user.target
Dir: &dalec.SourceInlineDir{

Files: map[string]*dalec.SourceInlineFile{

"foo.service": {
Contents: `
# simple-socket.service
Expand Down Expand Up @@ -1144,6 +1157,11 @@ Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/boot
})
})

t.Run("platform", func(t *testing.T) {
ctx := startTestSpan(ctx, t)
testPlatforms(ctx, t, testConfig)
})

t.Run("custom worker", func(t *testing.T) {
t.Parallel()
ctx := startTestSpan(baseCtx, t)
Expand Down Expand Up @@ -1246,6 +1264,128 @@ func testCustomLinuxWorker(ctx context.Context, t *testing.T, targetCfg targetCo
// Unfortunately it seems like there is an issue with the gateway client passing
// in source policies.
})

}

func testPlatforms(ctx context.Context, t *testing.T, testConfig testLinuxConfig) {
t.Run("build against different platform", func(t *testing.T) {
t.Parallel()

ls, err := testEnv.Platforms(ctx)
if err != nil {
t.Fatal(err)
}
if len(ls) <= 1 {
t.Skipf("builder does not support multiple platforms: %s", platformsAsStringer(ls))
}

testEnv.RunTest(ctx, t, func(ctx context.Context, client gwclient.Client) {
p := readDefaultPlatform(ctx, t, client)

matcher := platforms.OnlyStrict(p)
var testPlatform *ocispecs.Platform
for _, p2 := range ls {
// Get the first platform that is not the host platform that matches a supported distro platform
if !matcher.Match(p2) && testConfig.SupportedPlatforms.Match(p2) {
testPlatform = &p2
break
}
}

if testPlatform == nil {
msg := "could not find a platform suitable for testing, host platform: %s, available: %s"
ps := platformStringer(p)
workerPlatforms := platformsAsStringer(ls)
if os.Getenv("DALEC_CI") != "" {
t.Fatalf(msg, ps, workerPlatforms)
}
t.Skipf(msg, ps, workerPlatforms)
}

spec := &dalec.Spec{
Name: "test-platforms",
Version: "0.0.1",
Revision: "1",
Description: "Testing building on platform different from host platform",
License: "MIT",
Dependencies: &dalec.PackageDependencies{
Build: map[string]dalec.PackageConstraints{
"golang": {},
},
},
Sources: map[string]dalec.Source{
"src": {
Inline: &dalec.SourceInline{
Dir: &dalec.SourceInlineDir{
Files: map[string]*dalec.SourceInlineFile{
"go.mod": {
Contents: "module test\n\ngo 1.21.6",
},
"main.go": {
Contents: "package main\n\nfunc main() {}\n",
},
},
},
},
},
},
Build: dalec.ArtifactBuild{
Steps: []dalec.BuildStep{
{Command: "cd src; go build -o /tmp/test"},
},
},
Artifacts: dalec.Artifacts{
Binaries: map[string]dalec.ArtifactConfig{
"/tmp/test": {},
},
},
}

tp := *testPlatform
req := newSolveRequest(withPlatform(tp), withSpec(ctx, t, spec), withBuildTarget(testConfig.Target.Container))
res := solveT(ctx, t, client, req)

imgPlatforms := readResultPlatforms(t, res)
if len(imgPlatforms) != 1 {
t.Fatal("expected image output to contain 1 platform")
}

if !platforms.OnlyStrict(tp).Match(imgPlatforms[0]) {
t.Errorf("Expected image platform %q, got: %q", platformStringer(tp), platformStringer(imgPlatforms[0]))
}

ref, err := res.SingleRef()
if err != nil {
t.Fatal(err)
}
if ref == nil {
t.Fatal("got empty reference -- most likely an empty (scratch) state was returned")
}

// Read the ELF header so we can determine what the target architecture is.
dt, err := ref.ReadFile(ctx, gwclient.ReadRequest{
Filename: "/usr/bin/test",
})
if err != nil {
t.Fatal(err)
}

f, err := elf.NewFile(bytes.NewReader(dt))
if err != nil {
t.Fatal(err)
}

check := ocispecs.Platform{
OS: "linux",
}
elfToPlatform(f, &check)

if !platforms.OnlyStrict(*testPlatform).Match(check) {
t.Fatalf("output binary has unexpected platform, expected: %s, got: %s", platformStringer(*testPlatform), platformStringer(check))
}
})
})

}

func testPinnedBuildDeps(ctx context.Context, t *testing.T, targetCfg targetConfig, workerCfg workerConfig) {
Expand Down
Loading

0 comments on commit a05e530

Please sign in to comment.