Skip to content

Commit

Permalink
verifier: verify platforms of the build result
Browse files Browse the repository at this point in the history
Check that the result returned from the frontend
matches the user request conventions and show a
warning if it doesn't.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
  • Loading branch information
tonistiigi committed May 10, 2024
1 parent dd6acb2 commit 282abd3
Show file tree
Hide file tree
Showing 5 changed files with 362 additions and 35 deletions.
40 changes: 5 additions & 35 deletions client/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (
"github.com/moby/buildkit/util/testutil/echoserver"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/moby/buildkit/util/testutil/workers"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/tonistiigi/fsutil"
Expand Down Expand Up @@ -210,43 +209,14 @@ func testWarnings(t *testing.T, sb integration.Sandbox) {
return r, nil
}

status := make(chan *SolveStatus)
statusDone := make(chan struct{})
done := make(chan struct{})
wc := newWarningsCapture()

var warnings []*VertexWarning
vertexes := map[digest.Digest]struct{}{}

go func() {
defer close(statusDone)
for {
select {
case st, ok := <-status:
if !ok {
return
}
for _, s := range st.Vertexes {
vertexes[s.Digest] = struct{}{}
}
warnings = append(warnings, st.Warnings...)
case <-done:
return
}
}
}()

_, err = c.Build(ctx, SolveOpt{}, product, b, status)
_, err = c.Build(ctx, SolveOpt{}, product, b, wc.status)
require.NoError(t, err)

select {
case <-statusDone:
case <-time.After(10 * time.Second):
close(done)
}

<-statusDone
warnings := wc.wait()

require.Equal(t, 1, len(vertexes))
require.Equal(t, 1, len(wc.vertexes))
require.Equal(t, 1, len(warnings))

w := warnings[0]
Expand All @@ -257,7 +227,7 @@ func testWarnings(t *testing.T, sb integration.Sandbox) {
require.Equal(t, "and more detail", string(w.Detail[1]))
require.Equal(t, "https://example.com", w.URL)
require.Equal(t, 3, w.Level)
_, ok := vertexes[w.Vertex]
_, ok := wc.vertexes[w.Vertex]
require.True(t, ok)

require.Equal(t, "mydockerfile", w.SourceInfo.Filename)
Expand Down
161 changes: 161 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
testSolverOptLocalDirsStillWorks,
testOCIIndexMediatype,
testLayerLimitOnMounts,
testFrontendVerifyPlatforms,
}

func TestIntegration(t *testing.T) {
Expand Down Expand Up @@ -9999,6 +10000,166 @@ func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) {
}
}

func testFrontendVerifyPlatforms(t *testing.T, sb integration.Sandbox) {
requiresLinux(t)
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
st := llb.Scratch().File(
llb.Mkfile("foo", 0600, []byte("data")),
)

def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}

return c.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
}

wc := newWarningsCapture()
_, err = c.Build(sb.Context(), SolveOpt{
FrontendAttrs: map[string]string{
"platform": "linux/amd64,linux/arm64",
},
}, "", frontend, wc.status)
require.NoError(t, err)
warnings := wc.wait()

require.Len(t, warnings, 1)
require.Contains(t, string(warnings[0].Short), "Multiple platforms requested but result result is not multi-platform")

wc = newWarningsCapture()
_, err = c.Build(sb.Context(), SolveOpt{
FrontendAttrs: map[string]string{},
}, "", frontend, wc.status)
require.NoError(t, err)

warnings = wc.wait()
require.Len(t, warnings, 0)

frontend = func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res := gateway.NewResult()
platformsToTest := []string{"linux/amd64", "linux/arm64"}
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(platformsToTest)),
}
for i, platform := range platformsToTest {
st := llb.Scratch().File(
llb.Mkfile("platform", 0600, []byte(platform)),
)

def, err := st.Marshal(ctx)
if err != nil {
return nil, err
}

r, err := c.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}

ref, err := r.SingleRef()
if err != nil {
return nil, err
}

_, err = ref.ToState()
if err != nil {
return nil, err
}
res.AddRef(platform, ref)

expPlatforms.Platforms[i] = exptypes.Platform{
ID: platform,
Platform: platforms.MustParse(platform),
}
}
dt, err := json.Marshal(expPlatforms)
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterPlatformsKey, dt)

return res, nil
}

wc = newWarningsCapture()
_, err = c.Build(sb.Context(), SolveOpt{
FrontendAttrs: map[string]string{
"platform": "linux/amd64,linux/arm64",
},
}, "", frontend, wc.status)
require.NoError(t, err)
warnings = wc.wait()

require.Len(t, warnings, 0)

wc = newWarningsCapture()
_, err = c.Build(sb.Context(), SolveOpt{
FrontendAttrs: map[string]string{},
}, "", frontend, wc.status)
require.NoError(t, err)

warnings = wc.wait()
require.Len(t, warnings, 1)
require.Contains(t, string(warnings[0].Short), "do not match result platforms linux/amd64,linux/arm64")
}

type warningsCapture struct {
status chan *SolveStatus
statusDone chan struct{}
done chan struct{}
warnings []*VertexWarning
vertexes map[digest.Digest]struct{}
}

func newWarningsCapture() *warningsCapture {
w := &warningsCapture{
status: make(chan *SolveStatus),
statusDone: make(chan struct{}),
done: make(chan struct{}),
vertexes: map[digest.Digest]struct{}{},
}

go func() {
defer close(w.statusDone)
for {
select {
case st, ok := <-w.status:
if !ok {
return
}
for _, s := range st.Vertexes {
w.vertexes[s.Digest] = struct{}{}
}
w.warnings = append(w.warnings, st.Warnings...)
case <-w.done:
return
}
}
}()

return w
}

func (w *warningsCapture) wait() []*VertexWarning {
select {
case <-w.statusDone:
case <-time.After(10 * time.Second):
close(w.done)
}

<-w.statusDone
return w.warnings
}

func ensureFile(t *testing.T, path string) {
st, err := os.Stat(path)
require.NoError(t, err, "expected file at %s", path)
Expand Down
59 changes: 59 additions & 0 deletions exporter/verifier/opts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package verifier

import (
"encoding/json"
"strings"

"github.com/containerd/containerd/platforms"
"github.com/moby/buildkit/solver/result"
)

const requestOptsKeys = "verifier.requestopts"

const (
platformsKey = "platform"
labelsPrefix = "label:"
keyRequestID = "requestid"
)

type RequestOpts struct {
Platforms []string
Labels map[string]string
Request string
}

func CaptureFrontendOpts[T comparable](m map[string]string, res *result.Result[T]) error {
req := &RequestOpts{}
if v, ok := m[platformsKey]; ok {
req.Platforms = strings.Split(v, ",")
} else {
req.Platforms = []string{platforms.Format(platforms.Normalize(platforms.DefaultSpec()))}
}

req.Labels = map[string]string{}
for k, v := range m {
if strings.HasPrefix(k, labelsPrefix) {
req.Labels[strings.TrimPrefix(k, labelsPrefix)] = v
}
}
req.Request = m[keyRequestID]

dt, err := json.Marshal(req)
if err != nil {
return err
}
res.AddMeta(requestOptsKeys, dt)
return nil
}

func getRequestOpts[T comparable](res *result.Result[T]) (*RequestOpts, error) {
dt, ok := res.Metadata[requestOptsKeys]
if !ok {
return nil, nil
}
req := &RequestOpts{}
if err := json.Unmarshal(dt, req); err != nil {
return nil, err
}
return req, nil
}
Loading

0 comments on commit 282abd3

Please sign in to comment.