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  }