mirror of
https://github.com/moby/buildkit.git
synced 2026-08-05 15:20:22 +00:00
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>
878 lines
23 KiB
Go
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)
|
|
}
|