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 }