github.com/dean7474/operator-registry@v1.21.1-0.20220418203638-d4717f98c2e5/pkg/image/registry_test.go (about)

     1  package image_test
     2  
     3  import (
     4  	"context"
     5  	"crypto/x509"
     6  	"errors"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"math"
    10  	"math/rand"
    11  	"net/http"
    12  	"os"
    13  	"sync"
    14  	"testing"
    15  
    16  	"github.com/docker/distribution"
    17  	"github.com/docker/distribution/configuration"
    18  	"github.com/docker/distribution/reference"
    19  	repositorymiddleware "github.com/docker/distribution/registry/middleware/repository"
    20  	"github.com/opencontainers/go-digest"
    21  	"github.com/sirupsen/logrus"
    22  	"github.com/stretchr/testify/require"
    23  	"golang.org/x/mod/sumdb/dirhash"
    24  
    25  	"github.com/operator-framework/operator-registry/pkg/image"
    26  	"github.com/operator-framework/operator-registry/pkg/image/containerdregistry"
    27  	libimage "github.com/operator-framework/operator-registry/pkg/lib/image"
    28  )
    29  
    30  // cleanupFunc is a function that cleans up after some test infra.
    31  type cleanupFunc func()
    32  
    33  // newRegistryFunc is a function that creates and returns a new image.Registry to test its cleanupFunc.
    34  type newRegistryFunc func(t *testing.T, cafile string) (image.Registry, cleanupFunc)
    35  
    36  func poolForCertFile(t *testing.T, file string) *x509.CertPool {
    37  	rootCAs := x509.NewCertPool()
    38  	certs, err := ioutil.ReadFile(file)
    39  	require.NoError(t, err)
    40  	require.True(t, rootCAs.AppendCertsFromPEM(certs))
    41  	return rootCAs
    42  }
    43  
    44  func TestRegistries(t *testing.T) {
    45  	registries := map[string]newRegistryFunc{
    46  		"containerd": func(t *testing.T, cafile string) (image.Registry, cleanupFunc) {
    47  			r, err := containerdregistry.NewRegistry(
    48  				containerdregistry.WithLog(logrus.New().WithField("test", t.Name())),
    49  				containerdregistry.WithCacheDir(fmt.Sprintf("cache-%x", rand.Int())),
    50  				containerdregistry.WithRootCAs(poolForCertFile(t, cafile)),
    51  			)
    52  			require.NoError(t, err)
    53  			cleanup := func() {
    54  				require.NoError(t, r.Destroy())
    55  			}
    56  
    57  			return r, cleanup
    58  		},
    59  		// TODO: enable docker tests - currently blocked on a cross-platform way to configure either insecure registries
    60  		// or CA certs
    61  		//"docker": func(t *testing.T, cafile string) (image.Registry, cleanupFunc) {
    62  		//	r, err := execregistry.NewRegistry(containertools.DockerTool,
    63  		//		logrus.New().WithField("test", t.Name()),
    64  		//		cafile,
    65  		//	)
    66  		//	require.NoError(t, err)
    67  		//	cleanup := func() {
    68  		//		require.NoError(t, r.Destroy())
    69  		//	}
    70  		//
    71  		//	return r, cleanup
    72  		//},
    73  		// TODO: Enable buildah tests
    74  		// func(t *testing.T) image.Registry {
    75  		// 	r, err := buildahregistry.NewRegistry(
    76  		// 		buildahregistry.WithLog(logrus.New().WithField("test", t.Name())),
    77  		// 		buildahregistry.WithCacheDir(fmt.Sprintf("cache-%x", rand.Int())),
    78  		// 	)
    79  		// 	require.NoError(t, err)
    80  
    81  		// 	return r
    82  		// },
    83  	}
    84  
    85  	for name, registry := range registries {
    86  		testPullAndUnpack(t, name, registry)
    87  	}
    88  }
    89  
    90  func testPullAndUnpack(t *testing.T, name string, newRegistry newRegistryFunc) {
    91  	type args struct {
    92  		dockerRootDir string
    93  		img           string
    94  		pullErrCount  int
    95  		pullErr       error
    96  	}
    97  	type expected struct {
    98  		checksum      string
    99  		pullAssertion require.ErrorAssertionFunc
   100  	}
   101  	tests := []struct {
   102  		description string
   103  		args        args
   104  		expected    expected
   105  	}{
   106  		{
   107  			description: fmt.Sprintf("%s/ByTag", name),
   108  			args: args{
   109  				dockerRootDir: "testdata/golden",
   110  				img:           "/olmtest/kiali:1.4.2",
   111  			},
   112  			expected: expected{
   113  				checksum:      dirChecksum(t, "testdata/golden/bundles/kiali"),
   114  				pullAssertion: require.NoError,
   115  			},
   116  		},
   117  		{
   118  			description: fmt.Sprintf("%s/ByDigest", name),
   119  			args: args{
   120  				dockerRootDir: "testdata/golden",
   121  				img:           "/olmtest/kiali@sha256:a1bec450c104ceddbb25b252275eb59f1f1e6ca68e0ced76462042f72f7057d8",
   122  			},
   123  			expected: expected{
   124  				checksum:      dirChecksum(t, "testdata/golden/bundles/kiali"),
   125  				pullAssertion: require.NoError,
   126  			},
   127  		},
   128  		{
   129  			description: fmt.Sprintf("%s/WithOneRetriableError", name),
   130  			args: args{
   131  				dockerRootDir: "testdata/golden",
   132  				img:           "/olmtest/kiali:1.4.2",
   133  				pullErrCount:  1,
   134  				pullErr:       errors.New("dummy"),
   135  			},
   136  			expected: expected{
   137  				checksum:      dirChecksum(t, "testdata/golden/bundles/kiali"),
   138  				pullAssertion: require.NoError,
   139  			},
   140  		},
   141  		// TODO: figure out how to have the server send a detectable non-retriable error.
   142  		//{
   143  		//  description: fmt.Sprintf("%s/WithNonRetriableError", name),
   144  		//	args: args{
   145  		//		dockerRootDir: "testdata/golden",
   146  		//		img:           "/olmtest/kiali:1.4.2",
   147  		//	},
   148  		//	expected: expected{
   149  		//		pullAssertion: require.Error,
   150  		//	},
   151  		//},
   152  		{
   153  			description: fmt.Sprintf("%s/WithAlwaysRetriableError", name),
   154  			args: args{
   155  				dockerRootDir: "testdata/golden",
   156  				img:           "/olmtest/kiali:1.4.2",
   157  				pullErrCount:  math.MaxInt64,
   158  				pullErr:       errors.New("dummy"),
   159  			},
   160  			expected: expected{
   161  				pullAssertion: require.Error,
   162  			},
   163  		},
   164  	}
   165  	for _, tt := range tests {
   166  		t.Run(tt.description, func(t *testing.T) {
   167  			logrus.SetLevel(logrus.DebugLevel)
   168  			ctx, close := context.WithCancel(context.Background())
   169  			defer close()
   170  
   171  			configOpts := []libimage.ConfigOpt{}
   172  
   173  			if tt.args.pullErrCount > 0 {
   174  				configOpts = append(configOpts, func(config *configuration.Configuration) {
   175  					if config.Middleware == nil {
   176  						config.Middleware = make(map[string][]configuration.Middleware)
   177  					}
   178  
   179  					mockRepo := &mockRepo{blobStore: &mockBlobStore{
   180  						maxCount: tt.args.pullErrCount,
   181  						err:      tt.args.pullErr,
   182  					}}
   183  					middlewareName := fmt.Sprintf("test-%x", rand.Int())
   184  					require.NoError(t, repositorymiddleware.Register(middlewareName, mockRepo.init))
   185  					config.Middleware["repository"] = append(config.Middleware["repository"], configuration.Middleware{
   186  						Name: middlewareName,
   187  					})
   188  				})
   189  			}
   190  
   191  			host, cafile, err := libimage.RunDockerRegistry(ctx, tt.args.dockerRootDir, configOpts...)
   192  			require.NoError(t, err)
   193  
   194  			r, cleanup := newRegistry(t, cafile)
   195  			defer cleanup()
   196  
   197  			ref := image.SimpleReference(host + tt.args.img)
   198  			tt.expected.pullAssertion(t, r.Pull(ctx, ref))
   199  
   200  			if tt.expected.checksum != "" {
   201  				// Copy golden manifests to a temp dir
   202  				dir := "kiali-unpacked"
   203  				require.NoError(t, r.Unpack(ctx, ref, dir))
   204  
   205  				checksum := dirChecksum(t, dir)
   206  				require.Equal(t, tt.expected.checksum, checksum)
   207  
   208  				require.NoError(t, os.RemoveAll(dir))
   209  			}
   210  		})
   211  	}
   212  }
   213  
   214  func dirChecksum(t *testing.T, dir string) string {
   215  	sum, err := dirhash.HashDir(dir, "", dirhash.DefaultHash)
   216  	require.NoError(t, err)
   217  	return sum
   218  }
   219  
   220  var _ distribution.Repository = &mockRepo{}
   221  
   222  type mockRepo struct {
   223  	base      distribution.Repository
   224  	blobStore *mockBlobStore
   225  	once      sync.Once
   226  }
   227  
   228  func (f *mockRepo) init(ctx context.Context, base distribution.Repository, options map[string]interface{}) (distribution.Repository, error) {
   229  	f.once.Do(func() {
   230  		f.base = base
   231  		f.blobStore.base = base.Blobs(ctx)
   232  	})
   233  	return f, nil
   234  }
   235  
   236  func (f *mockRepo) Named() reference.Named {
   237  	return f.base.Named()
   238  }
   239  
   240  func (f *mockRepo) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
   241  	return f.base.Manifests(ctx, options...)
   242  }
   243  
   244  func (f *mockRepo) Blobs(ctx context.Context) distribution.BlobStore {
   245  	return f.blobStore
   246  }
   247  
   248  func (f *mockRepo) Tags(ctx context.Context) distribution.TagService {
   249  	return f.base.Tags(ctx)
   250  }
   251  
   252  var _ distribution.BlobStore = &mockBlobStore{}
   253  
   254  type mockBlobStore struct {
   255  	base     distribution.BlobStore
   256  	err      error
   257  	maxCount int
   258  
   259  	count int
   260  	m     sync.Mutex
   261  }
   262  
   263  func (f *mockBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   264  	f.m.Lock()
   265  	defer f.m.Unlock()
   266  	f.count++
   267  	if f.count <= f.maxCount {
   268  		return distribution.Descriptor{}, f.err
   269  	}
   270  	return f.base.Stat(ctx, dgst)
   271  }
   272  
   273  func (f *mockBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
   274  	return f.base.Get(ctx, dgst)
   275  }
   276  
   277  func (f *mockBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
   278  	return f.base.Open(ctx, dgst)
   279  }
   280  
   281  func (f *mockBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
   282  	return f.base.Put(ctx, mediaType, p)
   283  }
   284  
   285  func (f *mockBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
   286  	return f.base.Create(ctx, options...)
   287  }
   288  
   289  func (f *mockBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
   290  	return f.base.Resume(ctx, id)
   291  }
   292  
   293  func (f *mockBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
   294  	return f.base.ServeBlob(ctx, w, r, dgst)
   295  }
   296  
   297  func (f *mockBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
   298  	return f.base.Delete(ctx, dgst)
   299  }