k8s.io/client-go@v0.31.1/discovery/cached/disk/round_tripper_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package disk 18 19 import ( 20 "bytes" 21 "crypto/sha256" 22 "io" 23 "net/http" 24 "net/url" 25 "os" 26 "path/filepath" 27 "testing" 28 29 "github.com/peterbourgon/diskv" 30 "github.com/stretchr/testify/assert" 31 ) 32 33 // copied from k8s.io/client-go/transport/round_trippers_test.go 34 type testRoundTripper struct { 35 Request *http.Request 36 Response *http.Response 37 Err error 38 } 39 40 func (rt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 41 rt.Request = req 42 return rt.Response, rt.Err 43 } 44 45 func BenchmarkDiskCache(b *testing.B) { 46 cacheDir, err := os.MkdirTemp("", "cache-rt") 47 if err != nil { 48 b.Fatal(err) 49 } 50 defer os.RemoveAll(cacheDir) 51 52 d := diskv.New(diskv.Options{ 53 PathPerm: os.FileMode(0750), 54 FilePerm: os.FileMode(0660), 55 BasePath: cacheDir, 56 TempDir: filepath.Join(cacheDir, ".diskv-temp"), 57 }) 58 59 k := "localhost:8080/apis/batch/v1.json" 60 v, err := os.ReadFile("../../testdata/apis/batch/v1.json") 61 if err != nil { 62 b.Fatal(err) 63 } 64 65 c := sumDiskCache{disk: d} 66 67 for n := 0; n < b.N; n++ { 68 c.Set(k, v) 69 c.Get(k) 70 c.Delete(k) 71 } 72 } 73 74 func TestCacheRoundTripper(t *testing.T) { 75 rt := &testRoundTripper{} 76 cacheDir, err := os.MkdirTemp("", "cache-rt") 77 defer os.RemoveAll(cacheDir) 78 if err != nil { 79 t.Fatal(err) 80 } 81 cache := newCacheRoundTripper(cacheDir, rt) 82 83 // First call, caches the response 84 req := &http.Request{ 85 Method: http.MethodGet, 86 URL: &url.URL{Host: "localhost"}, 87 } 88 rt.Response = &http.Response{ 89 Header: http.Header{"ETag": []string{`"123456"`}}, 90 Body: io.NopCloser(bytes.NewReader([]byte("Content"))), 91 StatusCode: http.StatusOK, 92 } 93 resp, err := cache.RoundTrip(req) 94 if err != nil { 95 t.Fatal(err) 96 } 97 content, err := io.ReadAll(resp.Body) 98 if err != nil { 99 t.Fatal(err) 100 } 101 if string(content) != "Content" { 102 t.Errorf(`Expected Body to be "Content", got %q`, string(content)) 103 } 104 105 // Second call, returns cached response 106 req = &http.Request{ 107 Method: http.MethodGet, 108 URL: &url.URL{Host: "localhost"}, 109 } 110 rt.Response = &http.Response{ 111 StatusCode: http.StatusNotModified, 112 Body: io.NopCloser(bytes.NewReader([]byte("Other Content"))), 113 } 114 115 resp, err = cache.RoundTrip(req) 116 if err != nil { 117 t.Fatal(err) 118 } 119 120 // Read body and make sure we have the initial content 121 content, err = io.ReadAll(resp.Body) 122 resp.Body.Close() 123 if err != nil { 124 t.Fatal(err) 125 } 126 if string(content) != "Content" { 127 t.Errorf("Invalid content read from cache %q", string(content)) 128 } 129 } 130 131 func TestCacheRoundTripperPathPerm(t *testing.T) { 132 assert := assert.New(t) 133 134 rt := &testRoundTripper{} 135 cacheDir, err := os.MkdirTemp("", "cache-rt") 136 os.RemoveAll(cacheDir) 137 defer os.RemoveAll(cacheDir) 138 139 if err != nil { 140 t.Fatal(err) 141 } 142 cache := newCacheRoundTripper(cacheDir, rt) 143 144 // First call, caches the response 145 req := &http.Request{ 146 Method: http.MethodGet, 147 URL: &url.URL{Host: "localhost"}, 148 } 149 rt.Response = &http.Response{ 150 Header: http.Header{"ETag": []string{`"123456"`}}, 151 Body: io.NopCloser(bytes.NewReader([]byte("Content"))), 152 StatusCode: http.StatusOK, 153 } 154 resp, err := cache.RoundTrip(req) 155 if err != nil { 156 t.Fatal(err) 157 } 158 content, err := io.ReadAll(resp.Body) 159 if err != nil { 160 t.Fatal(err) 161 } 162 if string(content) != "Content" { 163 t.Errorf(`Expected Body to be "Content", got %q`, string(content)) 164 } 165 166 err = filepath.Walk(cacheDir, func(path string, info os.FileInfo, err error) error { 167 if err != nil { 168 return err 169 } 170 if info.IsDir() { 171 assert.Equal(os.FileMode(0750), info.Mode().Perm()) 172 } else { 173 assert.Equal(os.FileMode(0660), info.Mode().Perm()) 174 } 175 return nil 176 }) 177 assert.NoError(err) 178 } 179 180 func TestSumDiskCache(t *testing.T) { 181 assert := assert.New(t) 182 183 // Ensure that we'll return a cache miss if the backing file doesn't exist. 184 t.Run("NoSuchKey", func(t *testing.T) { 185 cacheDir, err := os.MkdirTemp("", "cache-test") 186 if err != nil { 187 t.Fatal(err) 188 } 189 defer os.RemoveAll(cacheDir) 190 d := diskv.New(diskv.Options{BasePath: cacheDir, TempDir: filepath.Join(cacheDir, ".diskv-temp")}) 191 c := &sumDiskCache{disk: d} 192 193 key := "testing" 194 195 got, ok := c.Get(key) 196 assert.False(ok) 197 assert.Equal([]byte{}, got) 198 }) 199 200 // Ensure that we'll return a cache miss if the backing file is empty. 201 t.Run("EmptyFile", func(t *testing.T) { 202 cacheDir, err := os.MkdirTemp("", "cache-test") 203 if err != nil { 204 t.Fatal(err) 205 } 206 defer os.RemoveAll(cacheDir) 207 d := diskv.New(diskv.Options{BasePath: cacheDir, TempDir: filepath.Join(cacheDir, ".diskv-temp")}) 208 c := &sumDiskCache{disk: d} 209 210 key := "testing" 211 212 f, err := os.Create(filepath.Join(cacheDir, sanitize(key))) 213 if err != nil { 214 t.Fatal(err) 215 } 216 f.Close() 217 218 got, ok := c.Get(key) 219 assert.False(ok) 220 assert.Equal([]byte{}, got) 221 }) 222 223 // Ensure that we'll return a cache miss if the backing has an invalid 224 // checksum. 225 t.Run("InvalidChecksum", func(t *testing.T) { 226 cacheDir, err := os.MkdirTemp("", "cache-test") 227 if err != nil { 228 t.Fatal(err) 229 } 230 defer os.RemoveAll(cacheDir) 231 d := diskv.New(diskv.Options{BasePath: cacheDir, TempDir: filepath.Join(cacheDir, ".diskv-temp")}) 232 c := &sumDiskCache{disk: d} 233 234 key := "testing" 235 value := []byte("testing") 236 mismatchedValue := []byte("testink") 237 sum := sha256.Sum256(value) 238 239 // Create a file with the sum of 'value' followed by the bytes of 240 // 'mismatchedValue'. 241 f, err := os.Create(filepath.Join(cacheDir, sanitize(key))) 242 if err != nil { 243 t.Fatal(err) 244 } 245 f.Write(sum[:]) 246 f.Write(mismatchedValue) 247 f.Close() 248 249 // The mismatched checksum should result in a cache miss. 250 got, ok := c.Get(key) 251 assert.False(ok) 252 assert.Equal([]byte{}, got) 253 }) 254 255 // Ensure that our disk cache will happily cache over the top of an existing 256 // value. We depend on this behaviour to recover from corrupted cache 257 // entries. When Get detects a bad checksum it will return a cache miss. 258 // This should cause httpcache to fall back to its underlying transport and 259 // to subsequently cache the new value, overwriting the corrupt one. 260 t.Run("OverwriteExistingKey", func(t *testing.T) { 261 cacheDir, err := os.MkdirTemp("", "cache-test") 262 if err != nil { 263 t.Fatal(err) 264 } 265 defer os.RemoveAll(cacheDir) 266 d := diskv.New(diskv.Options{BasePath: cacheDir, TempDir: filepath.Join(cacheDir, ".diskv-temp")}) 267 c := &sumDiskCache{disk: d} 268 269 key := "testing" 270 value := []byte("cool value!") 271 272 // Write a value. 273 c.Set(key, value) 274 got, ok := c.Get(key) 275 276 // Ensure we can read back what we wrote. 277 assert.True(ok) 278 assert.Equal(value, got) 279 280 differentValue := []byte("I'm different!") 281 282 // Write a different value. 283 c.Set(key, differentValue) 284 got, ok = c.Get(key) 285 286 // Ensure we can read back the different value. 287 assert.True(ok) 288 assert.Equal(differentValue, got) 289 }) 290 291 // Ensure that deleting a key does in fact delete it. 292 t.Run("DeleteKey", func(t *testing.T) { 293 cacheDir, err := os.MkdirTemp("", "cache-test") 294 if err != nil { 295 t.Fatal(err) 296 } 297 defer os.RemoveAll(cacheDir) 298 d := diskv.New(diskv.Options{BasePath: cacheDir, TempDir: filepath.Join(cacheDir, ".diskv-temp")}) 299 c := &sumDiskCache{disk: d} 300 301 key := "testing" 302 value := []byte("coolValue") 303 304 c.Set(key, value) 305 306 // Ensure we successfully set the value. 307 got, ok := c.Get(key) 308 assert.True(ok) 309 assert.Equal(value, got) 310 311 c.Delete(key) 312 313 // Ensure the value is gone. 314 got, ok = c.Get(key) 315 assert.False(ok) 316 assert.Equal([]byte{}, got) 317 318 // Ensure that deleting a non-existent value is a no-op. 319 c.Delete(key) 320 }) 321 }