372 lines
7.7 KiB
Go
372 lines
7.7 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/rand"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/containerd/containerd/content"
|
|
"github.com/moby/buildkit/util/contentutil"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Backend is the minimal interface that describes a testing backend.
|
|
type Backend interface {
|
|
Address() string
|
|
Rootless() bool
|
|
}
|
|
|
|
type Sandbox interface {
|
|
Backend
|
|
|
|
Cmd(...string) *exec.Cmd
|
|
PrintLogs(*testing.T)
|
|
NewRegistry() (string, error)
|
|
Value(string) interface{} // chosen matrix value
|
|
}
|
|
|
|
// BackendConfig is used to configure backends created by a worker.
|
|
type BackendConfig struct {
|
|
Logs map[string]*bytes.Buffer
|
|
ConfigFile string
|
|
}
|
|
|
|
type Worker interface {
|
|
New(*BackendConfig) (Backend, func() error, error)
|
|
Name() string
|
|
}
|
|
|
|
type ConfigUpdater interface {
|
|
UpdateConfigFile(string) string
|
|
}
|
|
|
|
type Test func(*testing.T, Sandbox)
|
|
|
|
var defaultWorkers []Worker
|
|
|
|
func Register(w Worker) {
|
|
defaultWorkers = append(defaultWorkers, w)
|
|
}
|
|
|
|
func List() []Worker {
|
|
return defaultWorkers
|
|
}
|
|
|
|
// TestOpt is an option that can be used to configure a set of integration
|
|
// tests.
|
|
type TestOpt func(*testConf)
|
|
|
|
func WithMatrix(key string, m map[string]interface{}) TestOpt {
|
|
return func(tc *testConf) {
|
|
if tc.matrix == nil {
|
|
tc.matrix = map[string]map[string]interface{}{}
|
|
}
|
|
tc.matrix[key] = m
|
|
}
|
|
}
|
|
|
|
func WithMirroredImages(m map[string]string) TestOpt {
|
|
return func(tc *testConf) {
|
|
if tc.mirroredImages == nil {
|
|
tc.mirroredImages = map[string]string{}
|
|
}
|
|
for k, v := range m {
|
|
tc.mirroredImages[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
type testConf struct {
|
|
matrix map[string]map[string]interface{}
|
|
mirroredImages map[string]string
|
|
}
|
|
|
|
func Run(t *testing.T, testCases []Test, opt ...TestOpt) {
|
|
if testing.Short() {
|
|
t.Skip("skipping in short mode")
|
|
}
|
|
|
|
if os.Getenv("SKIP_INTEGRATION_TESTS") == "1" {
|
|
t.Skip("skipping integration tests")
|
|
}
|
|
|
|
var tc testConf
|
|
for _, o := range opt {
|
|
o(&tc)
|
|
}
|
|
|
|
mirror, cleanup, err := runMirror(t, tc.mirroredImages)
|
|
require.NoError(t, err)
|
|
|
|
var mu sync.Mutex
|
|
var count int
|
|
cleanOnComplete := func() func() {
|
|
count++
|
|
return func() {
|
|
mu.Lock()
|
|
count--
|
|
if count == 0 {
|
|
cleanup()
|
|
}
|
|
mu.Unlock()
|
|
}
|
|
}
|
|
defer cleanOnComplete()()
|
|
|
|
matrix := prepareValueMatrix(tc)
|
|
|
|
list := List()
|
|
if os.Getenv("BUILDKIT_WORKER_RANDOM") == "1" && len(list) > 0 {
|
|
rand.Seed(time.Now().UnixNano())
|
|
list = []Worker{list[rand.Intn(len(list))]}
|
|
}
|
|
|
|
for _, br := range list {
|
|
for _, tc := range testCases {
|
|
for _, mv := range matrix {
|
|
fn := getFunctionName(tc)
|
|
name := fn + "/worker=" + br.Name() + mv.functionSuffix()
|
|
func(fn, testName string, br Worker, tc Test, mv matrixValue) {
|
|
ok := t.Run(testName, func(t *testing.T) {
|
|
defer cleanOnComplete()()
|
|
if !strings.HasSuffix(fn, "NoParallel") {
|
|
t.Parallel()
|
|
}
|
|
sb, closer, err := newSandbox(br, mirror, mv)
|
|
if err != nil {
|
|
if errors.Cause(err) == ErrorRequirements {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
}
|
|
defer func() {
|
|
assert.NoError(t, closer())
|
|
if t.Failed() {
|
|
sb.PrintLogs(t)
|
|
}
|
|
}()
|
|
tc(t, sb)
|
|
})
|
|
require.True(t, ok)
|
|
}(fn, name, br, tc, mv)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func getFunctionName(i interface{}) string {
|
|
fullname := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
|
dot := strings.LastIndex(fullname, ".") + 1
|
|
return strings.Title(fullname[dot:])
|
|
}
|
|
|
|
var localImageCache map[string]map[string]struct{}
|
|
|
|
func copyImagesLocal(t *testing.T, host string, images map[string]string) error {
|
|
for to, from := range images {
|
|
if localImageCache == nil {
|
|
localImageCache = map[string]map[string]struct{}{}
|
|
}
|
|
if _, ok := localImageCache[host]; !ok {
|
|
localImageCache[host] = map[string]struct{}{}
|
|
}
|
|
if _, ok := localImageCache[host][to]; ok {
|
|
continue
|
|
}
|
|
localImageCache[host][to] = struct{}{}
|
|
|
|
var desc ocispec.Descriptor
|
|
var provider content.Provider
|
|
var err error
|
|
if strings.HasPrefix(from, "local:") {
|
|
var closer func()
|
|
desc, provider, closer, err = providerFromBinary(strings.TrimPrefix(from, "local:"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if closer != nil {
|
|
defer closer()
|
|
}
|
|
} else {
|
|
desc, provider, err = contentutil.ProviderFromRef(from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
ingester, err := contentutil.IngesterFromRef(host + "/" + to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := contentutil.CopyChain(context.TODO(), ingester, provider, desc); err != nil {
|
|
return err
|
|
}
|
|
t.Logf("copied %s to local mirror %s", from, host+"/"+to)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func OfficialImages(names ...string) map[string]string {
|
|
ns := runtime.GOARCH
|
|
if ns == "arm64" {
|
|
ns = "arm64v8"
|
|
} else if ns != "amd64" && ns != "armhf" {
|
|
ns = "library"
|
|
}
|
|
m := map[string]string{}
|
|
for _, name := range names {
|
|
m["library/"+name] = "docker.io/" + ns + "/" + name
|
|
}
|
|
return m
|
|
}
|
|
|
|
func withMirrorConfig(mirror string) ConfigUpdater {
|
|
return mirrorConfig(mirror)
|
|
}
|
|
|
|
type mirrorConfig string
|
|
|
|
func (mc mirrorConfig) UpdateConfigFile(in string) string {
|
|
return fmt.Sprintf(`%s
|
|
|
|
[registry."docker.io"]
|
|
mirrors=["%s"]
|
|
`, in, mc)
|
|
}
|
|
|
|
func writeConfig(updaters []ConfigUpdater) (string, error) {
|
|
tmpdir, err := ioutil.TempDir("", "bktest_config")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if err := os.Chmod(tmpdir, 0711); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
s := ""
|
|
for _, upt := range updaters {
|
|
s = upt.UpdateConfigFile(s)
|
|
}
|
|
|
|
if err := ioutil.WriteFile(filepath.Join(tmpdir, buildkitdConfigFile), []byte(s), 0644); err != nil {
|
|
return "", err
|
|
}
|
|
return tmpdir, nil
|
|
}
|
|
|
|
func runMirror(t *testing.T, mirroredImages map[string]string) (host string, _ func() error, err error) {
|
|
mirrorDir := os.Getenv("BUILDKIT_REGISTRY_MIRROR_DIR")
|
|
|
|
var f *os.File
|
|
if mirrorDir != "" {
|
|
f, err = os.Create(filepath.Join(mirrorDir, "lock"))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
f.Close()
|
|
}
|
|
}()
|
|
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
mirror, cleanup, err := NewRegistry(mirrorDir)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
cleanup()
|
|
}
|
|
}()
|
|
|
|
if err := copyImagesLocal(t, mirror, mirroredImages); err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
if mirrorDir != "" {
|
|
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_UN); err != nil {
|
|
return "", nil, err
|
|
}
|
|
}
|
|
|
|
return mirror, cleanup, err
|
|
}
|
|
|
|
type matrixValue struct {
|
|
fn []string
|
|
values map[string]matrixValueChoice
|
|
}
|
|
|
|
func (mv matrixValue) functionSuffix() string {
|
|
if len(mv.fn) == 0 {
|
|
return ""
|
|
}
|
|
sort.Strings(mv.fn)
|
|
sb := &strings.Builder{}
|
|
for _, f := range mv.fn {
|
|
sb.Write([]byte("/" + f + "=" + mv.values[f].name))
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
type matrixValueChoice struct {
|
|
name string
|
|
value interface{}
|
|
}
|
|
|
|
func newMatrixValue(key, name string, v interface{}) matrixValue {
|
|
return matrixValue{
|
|
fn: []string{key},
|
|
values: map[string]matrixValueChoice{
|
|
key: {
|
|
name: name,
|
|
value: v,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func prepareValueMatrix(tc testConf) []matrixValue {
|
|
m := []matrixValue{}
|
|
for featureName, values := range tc.matrix {
|
|
current := m
|
|
m = []matrixValue{}
|
|
for featureValue, v := range values {
|
|
if len(current) == 0 {
|
|
m = append(m, newMatrixValue(featureName, featureValue, v))
|
|
}
|
|
for _, c := range current {
|
|
vv := newMatrixValue(featureName, featureValue, v)
|
|
vv.fn = append(vv.fn, c.fn...)
|
|
for k, v := range c.values {
|
|
vv.values[k] = v
|
|
}
|
|
m = append(m, vv)
|
|
}
|
|
}
|
|
}
|
|
if len(m) == 0 {
|
|
m = append(m, matrixValue{})
|
|
}
|
|
return m
|
|
}
|