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  }