diff --git a/client/llb/resolver.go b/client/llb/resolver.go index 73d89e19..31fc3959 100644 --- a/client/llb/resolver.go +++ b/client/llb/resolver.go @@ -14,6 +14,15 @@ func WithMetaResolver(mr ImageMetaResolver) ImageOption { }) } +// ResolveDigest uses the meta resolver to update the ref of image with full digest before marshaling. +// This makes image ref immutable and is recommended if you want to make sure meta resolver data +// matches the image used during the build. +func ResolveDigest(v bool) ImageOption { + return imageOptionFunc(func(ii *ImageInfo) { + ii.resolveDigest = v + }) +} + // ImageMetaResolver can resolve image config metadata from a reference type ImageMetaResolver interface { ResolveImageConfig(ctx context.Context, ref string, opt ResolveImageConfigOpt) (digest.Digest, []byte, error) diff --git a/client/llb/resolver_test.go b/client/llb/resolver_test.go new file mode 100644 index 00000000..cc138b48 --- /dev/null +++ b/client/llb/resolver_test.go @@ -0,0 +1,91 @@ +package llb + +import ( + "context" + "encoding/json" + "testing" + + "github.com/moby/buildkit/solver/pb" + digest "github.com/opencontainers/go-digest" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" +) + +func TestImageMetaResolver(t *testing.T) { + t.Parallel() + tr := &testResolver{ + digest: digest.FromBytes([]byte("foo")), + dir: "/bar", + } + st := Image("alpine", WithMetaResolver(tr)) + + require.Equal(t, false, tr.called) + + def, err := st.Marshal(context.TODO()) + require.NoError(t, err) + + require.Equal(t, true, tr.called) + + m, arr := parseDef(t, def.Def) + require.Equal(t, 2, len(arr)) + + dgst, idx := last(t, arr) + require.Equal(t, 0, idx) + require.Equal(t, m[dgst], arr[0]) + + require.Equal(t, "docker-image://docker.io/library/alpine:latest", arr[0].Op.(*pb.Op_Source).Source.GetIdentifier()) + + d, err := st.GetDir(context.TODO()) + require.NoError(t, err) + require.Equal(t, "/bar", d) +} + +func TestImageResolveDigest(t *testing.T) { + t.Parallel() + + st := Image("alpine", WithMetaResolver(&testResolver{ + digest: digest.FromBytes([]byte("bar")), + dir: "/foo", + }), ResolveDigest(true)) + + def, err := st.Marshal(context.TODO()) + require.NoError(t, err) + + m, arr := parseDef(t, def.Def) + require.Equal(t, 2, len(arr)) + + dgst, idx := last(t, arr) + require.Equal(t, 0, idx) + require.Equal(t, m[dgst], arr[0]) + + require.Equal(t, "docker-image://docker.io/library/alpine:latest@"+string(digest.FromBytes([]byte("bar"))), arr[0].Op.(*pb.Op_Source).Source.GetIdentifier()) + + d, err := st.GetDir(context.TODO()) + require.NoError(t, err) + require.Equal(t, "/foo", d) +} + +type testResolver struct { + digest digest.Digest + dir string + called bool +} + +func (r *testResolver) ResolveImageConfig(ctx context.Context, ref string, opt ResolveImageConfigOpt) (digest.Digest, []byte, error) { + var img struct { + Config struct { + Env []string `json:"Env,omitempty"` + WorkingDir string `json:"WorkingDir,omitempty"` + User string `json:"User,omitempty"` + } `json:"config,omitempty"` + } + r.called = true + + img.Config.WorkingDir = r.dir + + dt, err := json.Marshal(img) + if err != nil { + return "", nil, errors.WithStack(err) + } + return r.digest, dt, nil +} diff --git a/client/llb/source.go b/client/llb/source.go index 20ed8dba..b64cfbcc 100644 --- a/client/llb/source.go +++ b/client/llb/source.go @@ -92,7 +92,8 @@ func (s *SourceOp) Inputs() []Output { func Image(ref string, opts ...ImageOption) State { r, err := reference.ParseNormalizedNamed(ref) if err == nil { - ref = reference.TagNameOnly(r).String() + r = reference.TagNameOnly(r) + ref = r.String() } var info ImageInfo for _, opt := range opts { @@ -116,21 +117,35 @@ func Image(ref string, opts ...ImageOption) State { src := NewSource("docker-image://"+ref, attrs, info.Constraints) // controversial if err != nil { src.err = err - } - if info.metaResolver != nil { - _, dt, err := info.metaResolver.ResolveImageConfig(context.TODO(), ref, ResolveImageConfigOpt{ - Platform: info.Constraints.Platform, - ResolveMode: info.resolveMode.String(), - }) - if err != nil { - src.err = err - } else { - st, err := NewState(src.Output()).WithImageConfig(dt) - if err == nil { - return st - } - src.err = err + } else if info.metaResolver != nil { + if _, ok := r.(reference.Digested); ok || !info.resolveDigest { + return NewState(src.Output()).Async(func(ctx context.Context, st State) (State, error) { + _, dt, err := info.metaResolver.ResolveImageConfig(ctx, ref, ResolveImageConfigOpt{ + Platform: info.Constraints.Platform, + ResolveMode: info.resolveMode.String(), + }) + if err != nil { + return State{}, err + } + return st.WithImageConfig(dt) + }) } + return Scratch().Async(func(ctx context.Context, _ State) (State, error) { + dgst, dt, err := info.metaResolver.ResolveImageConfig(context.TODO(), ref, ResolveImageConfigOpt{ + Platform: info.Constraints.Platform, + ResolveMode: info.resolveMode.String(), + }) + if err != nil { + return State{}, err + } + if dgst != "" { + r, err = reference.WithDigest(r, dgst) + if err != nil { + return State{}, err + } + } + return NewState(NewSource("docker-image://"+r.String(), attrs, info.Constraints).Output()).WithImageConfig(dt) + }) } return NewState(src.Output()) } @@ -176,9 +191,10 @@ func (r ResolveMode) String() string { type ImageInfo struct { constraintsWrapper - metaResolver ImageMetaResolver - resolveMode ResolveMode - RecordType string + metaResolver ImageMetaResolver + resolveDigest bool + resolveMode ResolveMode + RecordType string } func Git(remote, ref string, opts ...GitOption) State {