github.com/google/cloudprober@v0.11.3/rds/file/file_test.go (about) 1 // Copyright 2021 The Cloudprober Authors. 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 file 16 17 import ( 18 "fmt" 19 "io/ioutil" 20 "os" 21 "strconv" 22 "testing" 23 "time" 24 25 configpb "github.com/google/cloudprober/rds/file/proto" 26 rdspb "github.com/google/cloudprober/rds/proto" 27 "google.golang.org/protobuf/proto" 28 ) 29 30 var testResourcesFiles = map[string][]string{ 31 "textpb": []string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 32 "json": []string{"testdata/targets.json"}, 33 } 34 35 var testExpectedResources = []*rdspb.Resource{ 36 { 37 Name: proto.String("switch-xx-1"), 38 Port: proto.Int32(8080), 39 Ip: proto.String("10.1.1.1"), 40 Labels: map[string]string{ 41 "device_type": "switch", 42 "cluster": "xx", 43 }, 44 }, 45 { 46 Name: proto.String("switch-xx-2"), 47 Port: proto.Int32(8081), 48 Ip: proto.String("10.1.1.2"), 49 Labels: map[string]string{ 50 "cluster": "xx", 51 }, 52 }, 53 { 54 Name: proto.String("switch-yy-1"), 55 Port: proto.Int32(8080), 56 Ip: proto.String("10.1.2.1"), 57 }, 58 { 59 Name: proto.String("switch-zz-1"), 60 Port: proto.Int32(8080), 61 Ip: proto.String("::aaa:1"), 62 }, 63 } 64 65 func compareResourceList(t *testing.T, got []*rdspb.Resource, want []*rdspb.Resource) { 66 t.Helper() 67 68 if len(got) != len(want) { 69 t.Fatalf("Got resources: %d, expected: %d", len(got), len(want)) 70 } 71 for i := range want { 72 if got[i].String() != want[i].String() { 73 t.Errorf("ListResources: got[%d]:\n%s\nexpected[%d]:\n%s", i, got[i].String(), i, want[i].String()) 74 } 75 } 76 } 77 78 func TestListResources(t *testing.T) { 79 for _, filetype := range []string{"textpb", "json"} { 80 t.Run(filetype, func(t *testing.T) { 81 p, err := New(&configpb.ProviderConfig{FilePath: testResourcesFiles[filetype]}, nil) 82 if err != nil { 83 t.Fatalf("Unexpected error while creating new provider: %v", err) 84 } 85 86 for _, test := range []struct { 87 desc string 88 resourcePath string 89 f []*rdspb.Filter 90 wantResources []*rdspb.Resource 91 }{ 92 { 93 desc: "no_filter", 94 wantResources: testExpectedResources, 95 }, 96 { 97 desc: "with_filter", 98 f: []*rdspb.Filter{ 99 { 100 Key: proto.String("labels.cluster"), 101 Value: proto.String("xx"), 102 }, 103 }, 104 wantResources: testExpectedResources[:2], 105 }, 106 } { 107 t.Run(test.desc, func(t *testing.T) { 108 got, err := p.ListResources(&rdspb.ListResourcesRequest{Filter: test.f}) 109 if err != nil { 110 t.Fatalf("Unexpected error while listing resources: %v", err) 111 } 112 compareResourceList(t, got.Resources, test.wantResources) 113 }) 114 } 115 }) 116 } 117 } 118 119 func TestListResourcesWithResourcePath(t *testing.T) { 120 p, err := New(&configpb.ProviderConfig{FilePath: testResourcesFiles["textpb"]}, nil) 121 if err != nil { 122 t.Fatalf("Unexpected error while creating new provider: %v", err) 123 } 124 got, err := p.ListResources(&rdspb.ListResourcesRequest{ResourcePath: proto.String(testResourcesFiles["textpb"][1])}) 125 if err != nil { 126 t.Fatalf("Unexpected error while listing resources: %v", err) 127 } 128 compareResourceList(t, got.Resources, testExpectedResources[2:]) 129 } 130 131 func BenchmarkListResources(b *testing.B) { 132 for _, n := range []int{100, 10000, 1000000} { 133 for _, filters := range [][]*rdspb.Filter{nil, []*rdspb.Filter{{Key: proto.String("name"), Value: proto.String("host-1.*")}}} { 134 b.Run(fmt.Sprintf("%d-resources,%d-filters", n, len(filters)), func(b *testing.B) { 135 b.StopTimer() 136 ls := &lister{ 137 resources: make([]*rdspb.Resource, n), 138 } 139 for i := 0; i < n; i++ { 140 ls.resources[i] = &rdspb.Resource{ 141 Name: proto.String(fmt.Sprintf("host-%d", i)), 142 Ip: proto.String("10.1.1.1"), 143 Port: proto.Int32(80), 144 Labels: map[string]string{ 145 "index": strconv.Itoa(i), 146 }, 147 LastUpdated: proto.Int64(time.Now().Unix()), 148 } 149 } 150 b.StartTimer() 151 152 for j := 0; j < b.N; j++ { 153 res, err := ls.listResources(&rdspb.ListResourcesRequest{ 154 Filter: filters, 155 }) 156 157 if err != nil { 158 b.Errorf("Unexpected error while listing resources: %v", err) 159 } 160 161 if filters == nil && len(res.GetResources()) != n { 162 b.Errorf("Got %d resources, wanted: %d", len(res.GetResources()), n) 163 } 164 } 165 }) 166 } 167 } 168 } 169 170 func testModTimeCheckBehavior(t *testing.T, disableModTimeCheck bool) { 171 t.Helper() 172 // Set up test file. 173 tf, err := ioutil.TempFile("", "cloudprober_rds_file.*.json") 174 if err != nil { 175 t.Fatal(err) 176 } 177 178 testFile := tf.Name() 179 defer os.Remove(tf.Name()) 180 181 b, err := ioutil.ReadFile(testResourcesFiles["json"][0]) 182 if err != nil { 183 t.Fatal(err) 184 } 185 ioutil.WriteFile(testFile, b, 0) 186 187 ls, err := newLister(testFile, &configpb.ProviderConfig{ 188 DisableModifiedTimeCheck: proto.Bool(disableModTimeCheck), 189 }, nil) 190 if err != nil { 191 t.Fatalf("Error creating file lister: %v", err) 192 } 193 194 // Step 1: Very first run. File should be loaded. 195 res, err := ls.listResources(nil) 196 if err != nil { 197 t.Errorf("Unexxpected error: %v", err) 198 } 199 if len(res.GetResources()) == 0 { 200 t.Error("Got no resources.") 201 } 202 wantResources := res 203 firstUpdateTime := ls.lastUpdated 204 205 // Step 2: 2nd run. File shouldn't reload unless disableModTimeCheck is true. 206 // Wait for a second and refresh again. 207 time.Sleep(time.Second) 208 ls.refresh() 209 210 if !disableModTimeCheck { 211 if ls.lastUpdated != firstUpdateTime { 212 t.Errorf("File unexpectedly reloaded. Update time: %v, last update time: %v", ls.lastUpdated, firstUpdateTime) 213 } 214 } else { 215 if ls.lastUpdated == firstUpdateTime { 216 t.Errorf("File unexpectly didn't reload. Update time: %v, last update time: %v", ls.lastUpdated, firstUpdateTime) 217 } 218 } 219 res, err = ls.listResources(nil) 220 if err != nil { 221 t.Errorf("Unexpected error: %v", err) 222 } 223 wantResources.LastModified = proto.Int64(ls.lastModified()) 224 if !proto.Equal(res, wantResources) { 225 t.Errorf("Got resources:\n%s\nWant resources:\n%s", res.String(), wantResources.String()) 226 } 227 228 // Step 3: Third run. It should reload file. 229 // Update file's modified time and see if file is reloaded. 230 fileModTime := time.Now() 231 if err := os.Chtimes(testFile, fileModTime, fileModTime); err != nil { 232 t.Logf("Error setting modified time on the test file: %v. Finishing test early.", err) 233 return 234 } 235 ls.refresh() 236 237 if ls.lastUpdated.Before(fileModTime) { 238 t.Errorf("File lister last update time (%v) before file mod time (%v)", ls.lastUpdated, fileModTime) 239 } 240 res, err = ls.listResources(nil) 241 if err != nil { 242 t.Errorf("Unexxpected error: %v", err) 243 } 244 wantResources.LastModified = proto.Int64(ls.lastModified()) 245 if !proto.Equal(res, wantResources) { 246 t.Errorf("Got resources:\n%s\nWant resources:\n%s", res.String(), wantResources.String()) 247 } 248 } 249 250 func TestModTimeCheckBehavior(t *testing.T) { 251 t.Run("default", func(t *testing.T) { 252 testModTimeCheckBehavior(t, false) 253 }) 254 255 t.Run("ignore-mod-time", func(t *testing.T) { 256 testModTimeCheckBehavior(t, true) 257 }) 258 } 259 260 func TestListResourcesWithCache(t *testing.T) { 261 // We test with a provider that contains two listers (created from textpb 262 // files above). We try accessing single lister (by setting resource path) 263 // and both listers. 264 tests := []struct { 265 desc string 266 filePaths [2]string // Lister's file paths. 267 listerLastModified [2]int64 // Last modified timestamp for listers. 268 ifModifiedSince int64 // Request's if_modified_since 269 resourcePath string // Request's resource path 270 wantResponse *rdspb.ListResourcesResponse 271 }{ 272 { 273 desc: "no-caching-all-resources", 274 filePaths: [2]string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 275 wantResponse: &rdspb.ListResourcesResponse{ 276 Resources: testExpectedResources, 277 LastModified: proto.Int64(0), 278 }, 279 }, 280 { 281 desc: "non-zero-last-modified,return-all-resources", 282 filePaths: [2]string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 283 listerLastModified: [2]int64{300, 314}, 284 wantResponse: &rdspb.ListResourcesResponse{ 285 Resources: testExpectedResources, 286 LastModified: proto.Int64(314), 287 }, 288 }, 289 { 290 desc: "if-modified-since-older-1,return-all-resources", 291 filePaths: [2]string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 292 listerLastModified: [2]int64{300, 314}, 293 ifModifiedSince: 300, 294 wantResponse: &rdspb.ListResourcesResponse{ 295 Resources: testExpectedResources, 296 LastModified: proto.Int64(314), 297 }, 298 }, 299 { 300 desc: "if-modified-since-older-2,return-all-resources", 301 filePaths: [2]string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 302 listerLastModified: [2]int64{300, 314}, 303 ifModifiedSince: 302, 304 wantResponse: &rdspb.ListResourcesResponse{ 305 Resources: testExpectedResources, 306 LastModified: proto.Int64(314), 307 }, 308 }, 309 { 310 desc: "one-resource-path-1st-file,cached", 311 filePaths: [2]string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 312 listerLastModified: [2]int64{300, 314}, 313 ifModifiedSince: 300, 314 resourcePath: "testdata/targets1.textpb", 315 wantResponse: &rdspb.ListResourcesResponse{ 316 LastModified: proto.Int64(300), 317 }, 318 }, 319 { 320 desc: "one-resource-path-2nd-file,uncached", 321 filePaths: [2]string{"testdata/targets1.textpb", "testdata/targets2.textpb"}, 322 listerLastModified: [2]int64{300, 314}, 323 ifModifiedSince: 300, 324 resourcePath: "testdata/targets2.textpb", 325 wantResponse: &rdspb.ListResourcesResponse{ 326 Resources: testExpectedResources[2:], 327 LastModified: proto.Int64(314), 328 }, 329 }, 330 { 331 desc: "if-modified-since-equal-no-resources", 332 ifModifiedSince: 314, 333 listerLastModified: [2]int64{300, 314}, 334 wantResponse: &rdspb.ListResourcesResponse{ 335 LastModified: proto.Int64(314), 336 }, 337 }, 338 { 339 desc: "if-modified-since-bigger-no-resources", 340 ifModifiedSince: 315, 341 listerLastModified: [2]int64{300, 314}, 342 wantResponse: &rdspb.ListResourcesResponse{ 343 LastModified: proto.Int64(314), 344 }, 345 }, 346 } 347 348 for _, test := range tests { 349 t.Run(test.desc, func(t *testing.T) { 350 p := &Provider{ 351 filePaths: test.filePaths[:], 352 listers: make(map[string]*lister), 353 } 354 355 for i, fp := range test.filePaths { 356 ls, _ := newLister(fp, &configpb.ProviderConfig{}, nil) 357 ls.lastUpdated = time.Unix(test.listerLastModified[i], 0) 358 p.listers[fp] = ls 359 } 360 361 resp, err := p.ListResources(&rdspb.ListResourcesRequest{ 362 ResourcePath: proto.String(test.resourcePath), 363 IfModifiedSince: proto.Int64(test.ifModifiedSince), 364 }) 365 366 if err != nil { 367 t.Errorf("Got unexpected error: %v", err) 368 return 369 } 370 371 if !proto.Equal(resp, test.wantResponse) { 372 t.Errorf("Got response:\n%s\nwanted:\n%s", resp.String(), test.wantResponse.String()) 373 } 374 }) 375 } 376 }