cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociserver/proxy_test.go (about) 1 // Copyright 2023 CUE Labs AG 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package ociserver_test 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "net/http" 22 "net/http/httptest" 23 "testing" 24 25 "github.com/go-quicktest/qt" 26 "github.com/opencontainers/go-digest" 27 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 28 29 "cuelabs.dev/go/oci/ociregistry" 30 "cuelabs.dev/go/oci/ociregistry/ociclient" 31 "cuelabs.dev/go/oci/ociregistry/ocimem" 32 "cuelabs.dev/go/oci/ociregistry/ociserver" 33 ) 34 35 // Test that implementing an OCI registry proxy by sitting ociserver 36 // in front of ociclient doesn't introduce extra HTTP requests to the proxy backend. 37 // 38 // Each test case begins with a backend registry (ociserver in front of ocimem) 39 // with an HTTP middleware to record backend requests as they come in. 40 // 41 // We then set up a proxy (ociserver in front of ociclient) where the client points at the backend, 42 // and the server has a similar middleware to record the proxy requests as they come in. 43 // 44 // Finally, we have an ociclient pointing at the proxy which performs an OCI action via clientDo. 45 // We expect the proxy and backend requests to be practically the same 46 // as long as ociserver and ociclient do the right thing. 47 48 // ociclient defaults to a chunk size of 64KiB. 49 // We want our small data to fit in a single chunk, 50 // and large data to need at least three chunks to properly test PATCH edge cases. 51 var ( 52 smallData = bytes.Repeat([]byte("x"), 10) // 10 B 53 largeData = bytes.Repeat([]byte("x"), 150*1024) // 150 KiB 54 ) 55 56 var proxyTests = []struct { 57 name string 58 clientDo func(context.Context, ociregistry.Interface) error 59 60 proxyRequests []string 61 backendRequests []string 62 }{ 63 { 64 name: "PushBlob_small", 65 clientDo: func(ctx context.Context, client ociregistry.Interface) error { 66 _, err := client.PushBlob(ctx, "foo/bar", ocispec.Descriptor{ 67 Size: int64(len(smallData)), 68 Digest: digest.FromBytes(smallData), 69 }, bytes.NewReader(smallData)) 70 return err 71 }, 72 proxyRequests: []string{ 73 "POST len=0", 74 "PUT len=10", 75 }, 76 backendRequests: []string{ 77 "POST len=0", 78 "PUT len=10", 79 }, 80 }, 81 { 82 name: "PushBlob_large", 83 clientDo: func(ctx context.Context, client ociregistry.Interface) error { 84 _, err := client.PushBlob(ctx, "foo/bar", ocispec.Descriptor{ 85 Size: int64(len(largeData)), 86 Digest: digest.FromBytes(largeData), 87 }, bytes.NewReader(largeData)) 88 return err 89 }, 90 proxyRequests: []string{ 91 "POST len=0", 92 "PUT len=153600", 93 }, 94 backendRequests: []string{ 95 "POST len=0", 96 "PUT len=153600", 97 }, 98 }, 99 { 100 name: "PushBlobChunked_large_oneWrite", 101 clientDo: func(ctx context.Context, client ociregistry.Interface) error { 102 bw, err := client.PushBlobChunked(ctx, "foo/bar", 0) 103 if err != nil { 104 return err 105 } 106 if _, err := bw.Write(largeData); err != nil { 107 return err 108 } 109 if _, err := bw.Commit(digest.FromBytes(largeData)); err != nil { 110 return err 111 } 112 return nil 113 }, 114 proxyRequests: []string{ 115 "POST len=0", 116 "PATCH len=153600", 117 "PUT len=0", 118 }, 119 backendRequests: []string{ 120 "POST len=0", 121 "PATCH len=153600", 122 "PUT len=0", 123 }, 124 }, 125 } 126 127 func recordingServer(tb testing.TB, reqs *[]string, handler http.Handler) *httptest.Server { 128 recHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 *reqs = append(*reqs, fmt.Sprintf("%s len=%d", r.Method, r.ContentLength)) 130 handler.ServeHTTP(w, r) 131 }) 132 server := httptest.NewServer(recHandler) 133 tb.Cleanup(server.Close) 134 return server 135 } 136 137 func testClient(tb testing.TB, server *httptest.Server) ociregistry.Interface { 138 client, err := ociclient.New(server.Listener.Addr().String(), &ociclient.Options{ 139 Insecure: true, // since it's a local httptest server 140 }) 141 qt.Assert(tb, qt.IsNil(err)) 142 return client 143 } 144 145 func TestProxyRequests(t *testing.T) { 146 for _, test := range proxyTests { 147 t.Run(test.name, func(t *testing.T) { 148 // Set up the backend (ociserver + ocimem) 149 var proxyReqs, backendReqs []string 150 backendServer := recordingServer(t, &backendReqs, 151 ociserver.New(ocimem.New(), nil)) 152 153 // Set up the proxy (ociserver + ociclient). 154 proxyServer := recordingServer(t, &proxyReqs, 155 ociserver.New(testClient(t, backendServer), nil)) 156 157 // Set up the input client, mimicking the end user like cmd/cue. 158 inputClient := testClient(t, proxyServer) 159 160 // Run the input client action, and compare the results. 161 err := test.clientDo(context.TODO(), inputClient) 162 qt.Assert(t, qt.IsNil(err)) 163 164 qt.Check(t, qt.DeepEquals(proxyReqs, test.proxyRequests)) 165 qt.Check(t, qt.DeepEquals(backendReqs, test.backendRequests)) 166 }) 167 } 168 }