k8s.io/registry.k8s.io@v0.3.1/cmd/archeio/main_test.go (about)

     1  //go:build !nointegration
     2  // +build !nointegration
     3  
     4  /*
     5  Copyright 2022 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package main
    21  
    22  import (
    23  	"fmt"
    24  	"net/http"
    25  	"net/netip"
    26  	"os"
    27  	"os/exec"
    28  	"path/filepath"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/google/go-containerregistry/pkg/crane"
    33  	"github.com/google/go-containerregistry/pkg/v1/validate"
    34  
    35  	"k8s.io/registry.k8s.io/internal/integration"
    36  	"k8s.io/registry.k8s.io/pkg/net/cloudcidrs"
    37  )
    38  
    39  type integrationTestCase struct {
    40  	Name   string
    41  	FakeIP string
    42  	Image  string
    43  	Digest string
    44  }
    45  
    46  // TestIntegrationMain tests the entire, built binary with an integration
    47  // test, pulling images with crane
    48  func TestIntegrationMain(t *testing.T) {
    49  	// setup crane
    50  	rootDir, err := integration.ModuleRootDir()
    51  	if err != nil {
    52  		t.Fatalf("Failed to detect module root dir: %v", err)
    53  	}
    54  
    55  	// build binary
    56  	buildCmd := exec.Command("make", "archeio")
    57  	buildCmd.Dir = rootDir
    58  	if err := buildCmd.Run(); err != nil {
    59  		t.Fatalf("Failed to build archeio for integration testing: %v", err)
    60  	}
    61  
    62  	// start server in background
    63  	testPort := "61337"
    64  	testAddr := "localhost:" + testPort
    65  	serverErrChan := make(chan error)
    66  	serverCmd := exec.Command("./archeio", "-v=9")
    67  	serverCmd.Dir = filepath.Join(rootDir, "bin")
    68  	serverCmd.Env = append(serverCmd.Env, "PORT="+testPort)
    69  	serverCmd.Stderr = os.Stderr
    70  	go func() {
    71  		serverErrChan <- serverCmd.Start()
    72  		serverErrChan <- serverCmd.Wait()
    73  	}()
    74  	t.Cleanup(func() {
    75  		if err := serverCmd.Process.Signal(os.Interrupt); err != nil {
    76  			t.Fatalf("failed to signal archeio: %v", err)
    77  		}
    78  		if err := <-serverErrChan; err != nil {
    79  			t.Fatalf("archeio did not exit cleanly: %v", err)
    80  		}
    81  	})
    82  
    83  	// wait for server to be up and running
    84  	startErr := <-serverErrChan
    85  	if startErr != nil {
    86  		t.Fatalf("Failed to start archeio: %v", err)
    87  	}
    88  	if !tryUntil(time.Now().Add(time.Second), func() bool {
    89  		_, err := http.Get("http://" + testAddr + "/v2/")
    90  		return err == nil
    91  	}) {
    92  		t.Fatal("timed out waiting for archeio to be ready")
    93  	}
    94  
    95  	// perform many test pulls ...
    96  	testCases := makeTestCases(t)
    97  	for i := range testCases {
    98  		tc := testCases[i]
    99  		t.Run(tc.Name, func(t *testing.T) {
   100  			t.Parallel()
   101  			ref := testAddr + "/" + tc.Image
   102  			// ensure we supply fake IP info from test case
   103  			craneOpts := []crane.Option{crane.WithTransport(newFakeIPTransport(tc.FakeIP))}
   104  			// test fetching digest first
   105  			digest, err := crane.Digest(ref, craneOpts...)
   106  			if err != nil {
   107  				t.Errorf("Fetch digest for %q failed: %v", ref, err)
   108  			}
   109  			if digest != tc.Digest {
   110  				t.Errorf("Wrong digest for %q", ref)
   111  				t.Errorf("Received: %q", digest)
   112  				t.Errorf("Expected: %q", tc.Digest)
   113  			}
   114  			err = pull(ref, craneOpts...)
   115  			if err != nil {
   116  				t.Errorf("Pull for %q failed: %v", ref, err)
   117  			}
   118  		})
   119  	}
   120  }
   121  
   122  func makeTestCases(t *testing.T) []integrationTestCase {
   123  	// a few small images that we really should be able to pull
   124  	wellKnownImages := []struct {
   125  		Name   string
   126  		Digest string
   127  	}{
   128  		{
   129  			Name:   "pause:3.1",
   130  			Digest: "sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea",
   131  		},
   132  		{
   133  			Name:   "pause:3.9",
   134  			Digest: "sha256:7031c1b283388d2c2e09b57badb803c05ebed362dc88d84b480cc47f72a21097",
   135  		},
   136  	}
   137  
   138  	// collect interesting IPs after checking that they meet expectations
   139  	type interestingIP struct {
   140  		Name string
   141  		IP   string
   142  	}
   143  	interestingIPs := []interestingIP{}
   144  	cidrs := cloudcidrs.NewIPMapper()
   145  
   146  	// One for GCP because we host there and have code paths for this
   147  	const gcpIP = "35.220.26.1"
   148  	if info, matches := cidrs.GetIP(netip.MustParseAddr(gcpIP)); !matches || info.Cloud != cloudcidrs.GCP {
   149  		t.Fatalf("Expected %q to be a GCP IP but is not detected as one with current data", gcpIP)
   150  	}
   151  	interestingIPs = append(interestingIPs, interestingIP{Name: "GCP", IP: gcpIP})
   152  
   153  	// One for AWS because we host there and have code paths for this
   154  	const awsIP = "35.180.1.1"
   155  	if info, matches := cidrs.GetIP(netip.MustParseAddr(awsIP)); !matches || info.Cloud != cloudcidrs.AWS {
   156  		t.Fatalf("Expected %q to be an AWS IP but is not detected as one with current data", awsIP)
   157  	}
   158  	interestingIPs = append(interestingIPs, interestingIP{Name: "AWS", IP: awsIP})
   159  
   160  	// we obviously won't see this in the wild, but we also know
   161  	// it should not match GCP, AWS or any future providers
   162  	const externalIP = "192.168.0.1"
   163  	if _, matches := cidrs.GetIP(netip.MustParseAddr(externalIP)); matches {
   164  		t.Fatalf("Expected %q to not match any provider IP range but it dies", externalIP)
   165  	}
   166  	interestingIPs = append(interestingIPs, interestingIP{Name: "External", IP: externalIP})
   167  
   168  	// generate testcases from test data, for every interesting IP pull each image
   169  	testCases := []integrationTestCase{}
   170  	for _, image := range wellKnownImages {
   171  		for _, ip := range interestingIPs {
   172  			testCases = append(testCases, integrationTestCase{
   173  				Name:   fmt.Sprintf("IP:%s (%q),Image:%q", ip.Name, ip.IP, image.Name),
   174  				FakeIP: ip.IP,
   175  				Image:  image.Name,
   176  				Digest: image.Digest,
   177  			})
   178  		}
   179  	}
   180  	return testCases
   181  }
   182  
   183  func pull(image string, options ...crane.Option) error {
   184  	img, err := crane.Pull(image, options...)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	return validate.Image(img)
   189  }
   190  
   191  type fakeIPTransport struct {
   192  	fakeXForwardFor string
   193  	h               http.RoundTripper
   194  }
   195  
   196  var _ http.RoundTripper = &fakeIPTransport{}
   197  
   198  func (f *fakeIPTransport) RoundTrip(r *http.Request) (*http.Response, error) {
   199  	r.Header.Add("X-Forwarded-For", f.fakeXForwardFor)
   200  	return f.h.RoundTrip(r)
   201  }
   202  
   203  func newFakeIPTransport(fakeIP string) *fakeIPTransport {
   204  	return &fakeIPTransport{
   205  		fakeXForwardFor: fakeIP + ",0.0.0.0",
   206  		h:               http.DefaultTransport,
   207  	}
   208  }
   209  
   210  // helper that calls `try()` in a loop until the deadline `until`
   211  // has passed or `try()`returns true, returns whether try ever returned true
   212  func tryUntil(until time.Time, try func() bool) bool {
   213  	for until.After(time.Now()) {
   214  		if try() {
   215  			return true
   216  		}
   217  		time.Sleep(time.Millisecond * 10)
   218  	}
   219  	return false
   220  }