Files
buildkit/client/client_http_source_test.go
Tonis Tiigi 38f8536ee3 client: split client integration tests by area
Move client integration test declarations out of the large harness files into
focused files for cache, exporters, sources, mounts, gateway behavior, and
related client features.

Keep registration lists grouped by source file, move non-gateway tests out of
the gateway harness, and remove the now-empty build_test.go. This is intended
as a mechanical split with no test logic changes.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-07-06 22:19:20 -07:00

878 lines
23 KiB
Go

package client
import (
"bytes"
"compress/gzip"
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sync/atomic"
"testing"
"time"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/client/llb/sourceresolver"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/secrets/secretsprovider"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/testutil/httpserver"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/moby/buildkit/util/testutil/workers"
digest "github.com/opencontainers/go-digest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func testBuildHTTPSource(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
LastModified: &modTime,
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
// invalid URL first
st := llb.HTTP(server.URL + "/bar")
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid response status 404")
// first correct request
st = llb.HTTP(server.URL + "/foo")
def, err = st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
require.NoError(t, err)
require.Equal(t, 1, server.Stats("/foo").AllRequests)
require.Equal(t, 0, server.Stats("/foo").CachedRequests)
tmpdir := t.TempDir()
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: tmpdir,
},
},
}, nil)
require.NoError(t, err)
require.Equal(t, 2, server.Stats("/foo").AllRequests)
require.Equal(t, 1, server.Stats("/foo").CachedRequests)
dt, err := os.ReadFile(filepath.Join(tmpdir, "foo"))
require.NoError(t, err)
require.Equal(t, []byte("content1"), dt)
allReqs := server.Stats("/foo").Requests
require.Equal(t, 2, len(allReqs))
require.Equal(t, http.MethodGet, allReqs[0].Method)
require.Equal(t, "gzip", allReqs[0].Header.Get("Accept-Encoding"))
require.Equal(t, http.MethodHead, allReqs[1].Method)
require.Equal(t, "gzip", allReqs[1].Header.Get("Accept-Encoding"))
require.NoError(t, os.RemoveAll(filepath.Join(tmpdir, "foo")))
// update the content at the url to be gzipped now, the final output
// should remain the same
modTime = time.Now().Add(-23 * time.Hour)
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
_, err = gw.Write(resp.Content)
require.NoError(t, err)
require.NoError(t, gw.Close())
gzipBytes := buf.Bytes()
respGzip := &httpserver.Response{
Etag: identity.NewID(),
Content: gzipBytes,
LastModified: &modTime,
ContentEncoding: "gzip",
}
server.SetRoute("/foo", respGzip)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: tmpdir,
},
},
}, nil)
require.NoError(t, err)
require.Equal(t, 4, server.Stats("/foo").AllRequests)
require.Equal(t, 1, server.Stats("/foo").CachedRequests)
dt, err = os.ReadFile(filepath.Join(tmpdir, "foo"))
require.NoError(t, err)
require.Equal(t, resp.Content, dt)
allReqs = server.Stats("/foo").Requests
require.Equal(t, 4, len(allReqs))
require.Equal(t, http.MethodHead, allReqs[2].Method)
require.Equal(t, "gzip", allReqs[2].Header.Get("Accept-Encoding"))
require.Equal(t, http.MethodGet, allReqs[3].Method)
require.Equal(t, "gzip", allReqs[3].Header.Get("Accept-Encoding"))
// test extra options
// llb.Chown not supported on Windows
st = integration.UnixOrWindows(
llb.HTTP(server.URL+"/foo", llb.Filename("bar"), llb.Chmod(0741), llb.Chown(1000, 1000)),
llb.HTTP(server.URL+"/foo", llb.Filename("bar"), llb.Chmod(0741)),
)
def, err = st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: tmpdir,
},
},
}, nil)
require.NoError(t, err)
require.Equal(t, 5, server.Stats("/foo").AllRequests)
require.Equal(t, 1, server.Stats("/foo").CachedRequests)
dt, err = os.ReadFile(filepath.Join(tmpdir, "bar"))
require.NoError(t, err)
require.Equal(t, []byte("content1"), dt)
fi, err := os.Stat(filepath.Join(tmpdir, "bar"))
require.NoError(t, err)
require.Equal(t, fi.ModTime().Format(http.TimeFormat), modTime.Format(http.TimeFormat))
// no support for llb.Chmod on Windows, default is returned
fMode := integration.UnixOrWindows(0741, 0666)
require.Equal(t, fMode, int(fi.Mode()&0777))
checkAllReleasable(t, c, sb, true)
// TODO: check that second request was marked as cached
}
func testBuildHTTPSourceAuthHeaderSecret(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
modTime := time.Now().Add(-24 * time.Hour) // avoid false positive with current time
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
LastModified: &modTime,
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
st := llb.HTTP(server.URL+"/foo", llb.AuthHeaderSecret("http-secret"))
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(
sb.Context(),
def,
SolveOpt{
Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{
"http-secret": []byte("Bearer foo"),
})},
},
nil,
)
require.NoError(t, err)
allReqs := server.Stats("/foo").Requests
require.Equal(t, 1, len(allReqs))
require.Equal(t, http.MethodGet, allReqs[0].Method)
require.Equal(t, "Bearer foo", allReqs[0].Header.Get("Authorization"))
}
// docker/buildx#2803
func testBuildHTTPSourceEtagScope(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
sharedEtag := identity.NewID()
resp := &httpserver.Response{
Etag: sharedEtag,
Content: []byte("content1"),
LastModified: &modTime,
}
resp2 := &httpserver.Response{
Etag: sharedEtag,
Content: []byte("another"),
LastModified: &modTime,
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/one/foo": resp,
"/two/foo": resp2,
})
defer server.Close()
// first correct request
st := llb.HTTP(server.URL + "/one/foo")
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
out1 := t.TempDir()
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: out1,
},
},
}, nil)
require.NoError(t, err)
require.Equal(t, 1, server.Stats("/one/foo").AllRequests)
require.Equal(t, 0, server.Stats("/one/foo").CachedRequests)
dt, err := os.ReadFile(filepath.Join(out1, "foo"))
require.NoError(t, err)
require.Equal(t, []byte("content1"), dt)
st = llb.HTTP(server.URL + "/two/foo")
def, err = st.Marshal(sb.Context())
require.NoError(t, err)
out2 := t.TempDir()
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: out2,
},
},
}, nil)
require.NoError(t, err)
require.Equal(t, 1, server.Stats("/two/foo").AllRequests)
require.Equal(t, 0, server.Stats("/two/foo").CachedRequests)
dt, err = os.ReadFile(filepath.Join(out2, "foo"))
require.NoError(t, err)
require.Equal(t, []byte("another"), dt)
out2 = t.TempDir()
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: out2,
},
},
}, nil)
require.NoError(t, err)
require.Equal(t, 2, server.Stats("/two/foo").AllRequests)
require.Equal(t, 1, server.Stats("/two/foo").CachedRequests)
allReqs := server.Stats("/two/foo").Requests
require.Equal(t, 2, len(allReqs))
require.Equal(t, http.MethodGet, allReqs[0].Method)
require.Equal(t, "gzip", allReqs[0].Header.Get("Accept-Encoding"))
require.Equal(t, http.MethodHead, allReqs[1].Method)
require.Equal(t, "gzip", allReqs[1].Header.Get("Accept-Encoding"))
require.NoError(t, os.RemoveAll(filepath.Join(out2, "foo")))
}
func testBuildHTTPSourceHeader(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
LastModified: &modTime,
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
st := llb.HTTP(
server.URL+"/foo",
llb.Header(llb.HTTPHeader{
Accept: "application/vnd.foo",
UserAgent: "fooagent",
}),
)
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
require.NoError(t, err)
allReqs := server.Stats("/foo").Requests
require.Equal(t, 1, len(allReqs))
require.Equal(t, http.MethodGet, allReqs[0].Method)
require.Equal(t, "application/vnd.foo", allReqs[0].Header.Get("accept"))
require.Equal(t, "fooagent", allReqs[0].Header.Get("user-agent"))
}
func testBuildHTTPSourceHostTokenSecret(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
modTime := time.Now().Add(-24 * time.Hour) // avoid false positive with current time
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
LastModified: &modTime,
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
st := llb.HTTP(server.URL + "/foo")
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
_, err = c.Solve(
sb.Context(),
def,
SolveOpt{
Session: []session.Attachable{secretsprovider.FromMap(map[string][]byte{
"HTTP_AUTH_TOKEN_127.0.0.1": []byte("123456"),
})},
},
nil,
)
require.NoError(t, err)
allReqs := server.Stats("/foo").Requests
require.Equal(t, 1, len(allReqs))
require.Equal(t, http.MethodGet, allReqs[0].Method)
require.Equal(t, "Bearer 123456", allReqs[0].Header.Get("Authorization"))
}
func testBuildHTTPSourcePGPSignatureVerify(t *testing.T, sb integration.Sandbox) {
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
signFixturesPath, ok := os.LookupEnv("BUILDKIT_TEST_SIGN_FIXTURES")
if !ok {
t.Skip("missing BUILDKIT_TEST_SIGN_FIXTURES")
}
payload, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.http.artifact"))
require.NoError(t, err)
sigData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.http.artifact.asc"))
require.NoError(t, err)
pubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.gpg.pub"))
require.NoError(t, err)
wrongPubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user2.gpg.pub"))
require.NoError(t, err)
httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/artifact.txt" {
http.NotFound(w, r)
return
}
_, _ = w.Write(payload)
}))
defer httpSrv.Close()
solve := func(t *testing.T, state llb.State) error {
def, err := state.Marshal(sb.Context())
require.NoError(t, err)
tmpdir := t.TempDir()
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: tmpdir,
},
},
}, nil)
if err != nil {
return err
}
dt, err := os.ReadFile(filepath.Join(tmpdir, "artifact.txt"))
require.NoError(t, err)
require.Equal(t, payload, dt)
return nil
}
t.Run("valid-signature", func(t *testing.T) {
validState := llb.HTTP(
httpSrv.URL+"/artifact.txt",
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
PubKey: pubKeyData,
Signature: sigData,
}),
)
require.NoError(t, solve(t, validState))
})
t.Run("wrong-pubkey", func(t *testing.T) {
invalidState := llb.HTTP(
httpSrv.URL+"/artifact.txt",
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
PubKey: wrongPubKeyData,
Signature: sigData,
}),
)
err = solve(t, invalidState)
require.Error(t, err)
require.ErrorContains(t, err, "failed to verify pgp signature")
})
t.Run("concatenated-pubkeys-right-key-second", func(t *testing.T) {
mergedPubKeys := append(append([]byte{}, wrongPubKeyData...), '\n')
mergedPubKeys = append(mergedPubKeys, pubKeyData...)
state := llb.HTTP(
httpSrv.URL+"/artifact.txt",
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
PubKey: mergedPubKeys,
Signature: sigData,
}),
)
require.NoError(t, solve(t, state))
})
}
func testBuildHTTPSourceUnauthorizedChecksumRace(t *testing.T, sb integration.Sandbox) {
integration.SkipOnPlatform(t, "windows")
workers.CheckFeatureCompat(t, sb, workers.FeatureMergeDiff)
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
var requests atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if requests.Add(1) == 1 {
time.Sleep(200 * time.Millisecond)
}
w.WriteHeader(http.StatusUnauthorized)
}))
defer server.Close()
url := server.URL + "/test.txt"
lower := llb.Scratch()
// Reproduce the race between same-URL HTTP sources with and without
// checksum sharing the resolver cache while the server returns 401.
withoutChecksum := lower.File(llb.Copy(llb.HTTP(url), "test.txt", "/a.txt"))
withChecksum := lower.File(llb.Copy(llb.HTTP(url, llb.Checksum(digest.FromBytes(nil))), "test.txt", "/b.txt"))
st := llb.Merge([]llb.State{
llb.Diff(lower, withoutChecksum),
llb.Diff(lower, withChecksum),
})
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
for range 10 {
requests.Store(0)
_, err := c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: t.TempDir(),
},
},
}, nil)
require.Error(t, err)
require.ErrorContains(t, err, "invalid response status 401")
}
}
func testHTTPPruneAfterCacheKey(t *testing.T, sb integration.Sandbox) {
// this test depends on hitting race condition in internal functions.
// If debugging and expecting failure you can add small sleep in beginning of source/http.Exec() to hit reliably
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
done := make(chan struct{})
startScan := make(chan struct{})
stopScan := make(chan struct{})
pauseScan := make(chan struct{})
go func() {
// attempt to prune the HTTP record in between cachekey and snapshot
defer close(done)
for {
select {
case <-startScan:
scan:
for {
select {
case <-pauseScan:
break scan
default:
du, err := c.DiskUsage(ctx)
assert.NoError(t, err)
for _, entry := range du {
if entry.Description == "http url "+server.URL+"/foo" {
if !entry.InUse {
t.Logf("entry no longer in use, pruning")
err = c.Prune(ctx, nil)
assert.NoError(t, err)
resp.Etag = identity.NewID()
resp.Content = []byte("content2")
}
}
}
}
}
case <-stopScan:
return
}
}
}()
const iterations = 10
for range iterations {
startScan <- struct{}{}
resp.Etag = identity.NewID()
resp.Content = []byte("content1")
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
st := llb.Scratch().File(llb.Copy(llb.HTTP(server.URL+"/foo"), "foo", "bar"))
def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}
resp, err := c.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}
return resp, nil
}, nil)
require.NoError(t, err)
pauseScan <- struct{}{}
err = c.Prune(ctx, nil)
require.NoError(t, err)
checkAllReleasable(t, c, sb, false)
}
close(stopScan)
<-done
}
// testHTTPPruneAfterResolveMeta ensures that pruning after ResolveSourceMetadata
// doesn't pull in new data for same build. Once URL has been resolved once for a specific
// build, the data should be considered immutable and remote changes don't affect ongoing build.
func testHTTPPruneAfterResolveMeta(t *testing.T, sb integration.Sandbox) {
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
dest := t.TempDir()
_, err = c.Build(ctx, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: dest,
},
},
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
id := server.URL + "/foo"
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.HTTP)
// prune all
err = c.Prune(ctx, nil)
require.NoError(t, err)
resp.Content = []byte("content2") // etag is same so should hit cache if record not pruned
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}
return gc.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(dest, "bar"))
require.NoError(t, err)
require.Equal(t, "content1", string(dt))
checkAllReleasable(t, c, sb, false)
}
func testHTTPResolveMetaReuse(t *testing.T, sb integration.Sandbox) {
// the difference with testHTTPPruneAfterResolveMeta is that here we change content with the etag on the server
// but because the URL was already resolved once, the new content should not be seen
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
dest := t.TempDir()
_, err = c.Build(ctx, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: dest,
},
},
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
id := server.URL + "/foo"
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.HTTP)
resp.Etag = identity.NewID()
resp.Content = []byte("content2") // etag changed so new content would be returned if re-resolving
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}
return gc.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(dest, "bar"))
require.NoError(t, err)
require.Equal(t, "content1", string(dt))
}
// testHTTPResolveMultiBuild is a negative test for testHTTPResolveMetaReuse to ensure that
// URLs are resolved in between separate builds
func testHTTPResolveMultiBuild(t *testing.T, sb integration.Sandbox) {
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
})
defer server.Close()
dest := t.TempDir()
_, err = c.Build(ctx, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: dest,
},
},
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
id := server.URL + "/foo"
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.HTTP)
require.Equal(t, digest.FromBytes(resp.Content), md.HTTP.Digest)
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}
return gc.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(dest, "bar"))
require.NoError(t, err)
require.Equal(t, "content1", string(dt))
resp.Etag = identity.NewID()
resp.Content = []byte("content2")
_, err = c.Build(ctx, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: dest,
},
},
}, "test", func(ctx context.Context, gc gateway.Client) (*gateway.Result, error) {
id := server.URL + "/foo"
md, err := gc.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.HTTP)
require.Equal(t, digest.FromBytes(resp.Content), md.HTTP.Digest)
st := llb.Scratch().File(llb.Copy(llb.HTTP(id), "foo", "bar"))
def, err := st.Marshal(sb.Context())
if err != nil {
return nil, err
}
return gc.Solve(ctx, gateway.SolveRequest{
Definition: def.ToPB(),
})
}, nil)
require.NoError(t, err)
dt, err = os.ReadFile(filepath.Join(dest, "bar"))
require.NoError(t, err)
require.Equal(t, "content2", string(dt))
}
func testHTTPResolveSourceMetadata(t *testing.T, sb integration.Sandbox) {
ctx := sb.Context()
c, err := New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
modTime := time.Now().Add(-24 * time.Hour) // avoid falso positive with current time
resp := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content1"),
LastModified: &modTime,
}
resp2 := &httpserver.Response{
Etag: identity.NewID(),
Content: []byte("content2"),
ContentDisposition: "attachment; filename=\"my img.jpg\"",
}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/foo": resp,
"/bar": resp2,
})
defer server.Close()
_, err = c.Build(ctx, SolveOpt{}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
id := server.URL + "/foo"
md, err := c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.HTTP)
require.Equal(t, digest.FromBytes(resp.Content), md.HTTP.Digest)
require.Equal(t, "foo", md.HTTP.Filename)
require.NotNil(t, md.HTTP.LastModified)
require.Equal(t, modTime.Unix(), md.HTTP.LastModified.Unix())
require.Equal(t, id, md.Op.Identifier)
id = server.URL + "/bar"
md, err = c.ResolveSourceMetadata(ctx, &pb.SourceOp{
Identifier: id,
}, sourceresolver.Opt{})
if err != nil {
return nil, err
}
require.NotNil(t, md.HTTP)
require.Equal(t, digest.FromBytes(resp2.Content), md.HTTP.Digest)
require.Equal(t, "my img.jpg", md.HTTP.Filename)
require.Nil(t, md.HTTP.LastModified)
require.Equal(t, id, md.Op.Identifier)
return nil, nil
}, nil)
require.NoError(t, err)
}