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 }