github.com/google/cloudprober@v0.11.3/rds/gcp/gce_instances_test.go (about)

     1  // Copyright 2017-2020 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 gcp
    16  
    17  import (
    18  	"fmt"
    19  	"io/ioutil"
    20  	"net"
    21  	"net/http"
    22  	"reflect"
    23  	"sort"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/golang/protobuf/proto"
    28  	"github.com/google/cloudprober/logger"
    29  	pb "github.com/google/cloudprober/rds/proto"
    30  )
    31  
    32  type testNetIf struct {
    33  	privateIP    string
    34  	publicIP     string
    35  	aliasIPRange string
    36  	ipv6         string
    37  	publicIPv6   string
    38  }
    39  
    40  type testInstance struct {
    41  	name   string
    42  	zone   string
    43  	netIf  []*testNetIf
    44  	labels map[string]string
    45  }
    46  
    47  func (ti *testInstance) data() *instanceData {
    48  	var cNetIfs []networkInterface
    49  	for _, ni := range ti.netIf {
    50  		cNetIf := networkInterface{
    51  			NetworkIP:   ni.privateIP,
    52  			Ipv6Address: ni.ipv6,
    53  		}
    54  		if ni.publicIP != "" {
    55  			cNetIf.AccessConfigs = []accessConfig{
    56  				{NatIP: ni.publicIP},
    57  			}
    58  		}
    59  		if ni.publicIPv6 != "" {
    60  			cNetIf.Ipv6AccessConfigs = []accessConfig{
    61  				{ExternalIpv6: ni.publicIPv6},
    62  			}
    63  		}
    64  		if ni.aliasIPRange != "" {
    65  			cNetIf.AliasIPRanges = []struct {
    66  				IPCidrRange string `json:"ipCidrRange,omitempty"`
    67  			}{
    68  				{
    69  					IPCidrRange: ni.aliasIPRange,
    70  				},
    71  			}
    72  		}
    73  		cNetIfs = append(cNetIfs, cNetIf)
    74  	}
    75  
    76  	return &instanceData{ii: &instanceInfo{Name: ti.name, NetworkInterfaces: cNetIfs, Labels: ti.labels}}
    77  }
    78  
    79  func (ti *testInstance) expectedIP(spec *testSpec) string {
    80  	switch spec.ipType {
    81  	case "private":
    82  		if spec.ipv6 {
    83  			return ti.netIf[spec.index].ipv6
    84  		}
    85  		return ti.netIf[spec.index].privateIP
    86  	case "public":
    87  		if spec.ipv6 {
    88  			return ti.netIf[spec.index].publicIPv6
    89  		}
    90  		return ti.netIf[spec.index].publicIP
    91  	case "ipAliasRange":
    92  		netIP, _, _ := net.ParseCIDR(ti.netIf[spec.index].aliasIPRange)
    93  		if netIP == nil {
    94  			netIP = net.ParseIP(ti.netIf[spec.index].aliasIPRange)
    95  		}
    96  		return netIP.String()
    97  	}
    98  	return ""
    99  }
   100  
   101  var testInstancesData = []*testInstance{
   102  	{
   103  		name: "ins1",
   104  		zone: "z-1",
   105  		netIf: []*testNetIf{
   106  			&testNetIf{"10.216.0.1", "104.100.143.1", "192.168.1.0/24", "2600:2d00:4030:a47:c0a8:2110:0:0", "2600:2d00:4030:a47:c0a8:2110:1:0"},
   107  			&testNetIf{"10.216.1.1", "", "", "", ""},
   108  		},
   109  		labels: map[string]string{
   110  			"env":  "staging",
   111  			"func": "rds",
   112  		},
   113  	},
   114  	{
   115  		name: "ins2",
   116  		zone: "z-2",
   117  		netIf: []*testNetIf{
   118  			&testNetIf{"10.216.0.2", "104.100.143.2", "192.168.2.0", "", ""},
   119  			&testNetIf{"10.216.1.2", "104.100.143.3", "", "", ""},
   120  		},
   121  		labels: map[string]string{
   122  			"env":  "prod",
   123  			"func": "rds",
   124  		},
   125  	},
   126  	{
   127  		name: "ins3",
   128  		zone: "z-2",
   129  		netIf: []*testNetIf{
   130  			&testNetIf{"10.216.0.3", "104.100.143.4", "192.168.3.0", "", ""},
   131  			&testNetIf{"10.216.1.3", "104.100.143.5", "", "", ""},
   132  		},
   133  		labels: map[string]string{
   134  			"env":  "prod",
   135  			"func": "rds",
   136  		},
   137  	},
   138  }
   139  
   140  func testLister() *gceInstancesLister {
   141  	// Initialize instanceLister manually for testing, using the
   142  	// testInstances data. Default initialization invokes GCE APIs which we want
   143  	// to avoid.
   144  	lister := &gceInstancesLister{
   145  		cachePerScope: make(map[string]map[string]*instanceData),
   146  		namesPerScope: make(map[string][]string),
   147  		l:             &logger.Logger{},
   148  	}
   149  
   150  	for _, ti := range testInstancesData {
   151  		if lister.cachePerScope[ti.zone] == nil {
   152  			lister.cachePerScope[ti.zone] = make(map[string]*instanceData)
   153  		}
   154  		lister.cachePerScope[ti.zone][ti.name] = ti.data()
   155  		lister.namesPerScope[ti.zone] = append(lister.namesPerScope[ti.zone], ti.name)
   156  	}
   157  
   158  	return lister
   159  }
   160  
   161  type testSpec struct {
   162  	f      []*pb.Filter
   163  	index  int
   164  	ipType string
   165  	ipv6   bool
   166  	err    bool
   167  }
   168  
   169  func (ts *testSpec) ipConfig() *pb.IPConfig {
   170  	ipConfig := &pb.IPConfig{}
   171  	if ts.ipv6 {
   172  		ipConfig.IpVersion = pb.IPConfig_IPV6.Enum()
   173  	}
   174  	switch ts.ipType {
   175  	case "public":
   176  		ipConfig.IpType = pb.IPConfig_PUBLIC.Enum()
   177  	case "ipAliasRange":
   178  		ipConfig.IpType = pb.IPConfig_ALIAS.Enum()
   179  	}
   180  	if ts.index != 0 {
   181  		ipConfig.NicIndex = proto.Int32(int32(ts.index))
   182  	}
   183  	return ipConfig
   184  }
   185  
   186  func testListResources(t *testing.T, gil *gceInstancesLister, expectedInstances []*testInstance, spec *testSpec) {
   187  	t.Helper()
   188  
   189  	if spec == nil {
   190  		spec = &testSpec{}
   191  	}
   192  	if spec.ipType == "" {
   193  		spec.ipType = "private"
   194  	}
   195  
   196  	var filters []*pb.Filter
   197  	if spec.f != nil {
   198  		filters = append(filters, spec.f...)
   199  	}
   200  
   201  	resources, err := gil.listResources(&pb.ListResourcesRequest{
   202  		Filter:   filters,
   203  		IpConfig: spec.ipConfig(),
   204  	})
   205  
   206  	if err != nil {
   207  		if !spec.err {
   208  			t.Errorf("Got error while listing resources: %v, expected no errors", err)
   209  		}
   210  		return
   211  	}
   212  
   213  	if len(resources) != len(expectedInstances) {
   214  		t.Errorf("Got wrong number of targets. Expected: %d, Got: %d", len(expectedInstances), len(resources))
   215  	}
   216  
   217  	// Build a map from the resources in the reply.
   218  	instances := make(map[string]string)
   219  	for _, res := range resources {
   220  		instances[res.GetName()] = res.GetIp()
   221  	}
   222  
   223  	for _, ti := range expectedInstances {
   224  		ip := instances[ti.name]
   225  		expectedIP := ti.expectedIP(spec)
   226  		if ip != expectedIP {
   227  			t.Errorf("Got wrong <%s> IP for %s. Expected: %s, Got: %s", spec.ipType, ti.name, expectedIP, ip)
   228  		}
   229  	}
   230  }
   231  
   232  func TestInstancesResourcesFilters(t *testing.T) {
   233  	lister := testLister()
   234  
   235  	// #################################################################
   236  	// Instances, with first NIC and private IP. Expect no errors.
   237  	// #################################################################
   238  	testListResources(t, lister, testInstancesData, nil)
   239  
   240  	// #################################################################
   241  	// Instances, with first NIC and private IP, but a bad filter.
   242  	// #################################################################
   243  	f := []*pb.Filter{
   244  		{
   245  			Key:   proto.String("instance_name"),
   246  			Value: proto.String("ins2"),
   247  		},
   248  	}
   249  	testListResources(t, lister, testInstancesData[1:], &testSpec{f: f, err: true})
   250  
   251  	// #################################################################
   252  	// Instances, matching the name "ins." and labels: env:prod, func:rds
   253  	// Expect: all instances, no errors.
   254  	// #################################################################
   255  	f = []*pb.Filter{
   256  		{
   257  			Key:   proto.String("name"),
   258  			Value: proto.String("ins."),
   259  		},
   260  		{
   261  			Key:   proto.String("labels.func"),
   262  			Value: proto.String("rds"),
   263  		},
   264  	}
   265  	testListResources(t, lister, testInstancesData, &testSpec{f: f})
   266  
   267  	// #################################################################
   268  	// Instances, matching the name "ins." and labels: env:prod, func:rds
   269  	// Expect: Only second instance, no errors.
   270  	// #################################################################
   271  	f = []*pb.Filter{
   272  		{
   273  			Key:   proto.String("name"),
   274  			Value: proto.String("ins."),
   275  		},
   276  		{
   277  			Key:   proto.String("labels.env"),
   278  			Value: proto.String("prod"),
   279  		},
   280  		{
   281  			Key:   proto.String("labels.func"),
   282  			Value: proto.String("rds"),
   283  		},
   284  	}
   285  	testListResources(t, lister, testInstancesData[1:], &testSpec{f: f})
   286  
   287  	// #################################################################
   288  	// Instances, matching the name "ins." and labels: env:prod, func:server
   289  	// Expect: no instance, no errors.
   290  	// #################################################################
   291  	f = []*pb.Filter{
   292  		{
   293  			Key:   proto.String("name"),
   294  			Value: proto.String("ins."),
   295  		},
   296  		{
   297  			Key:   proto.String("labels.env"),
   298  			Value: proto.String("prod"),
   299  		},
   300  		{
   301  			Key:   proto.String("labels.func"),
   302  			Value: proto.String("server"),
   303  		},
   304  	}
   305  	testListResources(t, lister, []*testInstance{}, &testSpec{f: f})
   306  }
   307  
   308  func TestInstancesResources(t *testing.T) {
   309  	lister := testLister()
   310  
   311  	// #################################################################
   312  	// Instances, with first NIC and private IP. Expect no errors.
   313  	// #################################################################
   314  	testListResources(t, lister, testInstancesData, nil)
   315  
   316  	// #################################################################
   317  	// Instances, with first NIC and IPv6
   318  	// Expect no errors.
   319  	// #################################################################
   320  	testListResources(t, lister, testInstancesData, &testSpec{ipv6: true})
   321  
   322  	// #################################################################
   323  	// Instances, with first NIC and public IP
   324  	// Expect no errors.
   325  	// #################################################################
   326  	testListResources(t, lister, testInstancesData, &testSpec{ipType: "public"})
   327  
   328  	// #################################################################
   329  	// Instances (only ins1), with first NIC, IPv6 and public IP.
   330  	// Expect no errors.
   331  	// #################################################################
   332  	f := []*pb.Filter{
   333  		{
   334  			Key:   proto.String("name"),
   335  			Value: proto.String("ins1"),
   336  		},
   337  	}
   338  	testListResources(t, lister, testInstancesData[:1], &testSpec{f: f, ipType: "public", ipv6: true})
   339  
   340  	// #################################################################
   341  	// Instances, with first NIC and alias IP
   342  	// Expect no errors.
   343  	// #################################################################
   344  	testListResources(t, lister, testInstancesData, &testSpec{ipType: "ipAliasRange"})
   345  
   346  	// #################################################################
   347  	// Instances, with second NIC and private IP
   348  	// Expect no errors.
   349  	// #################################################################
   350  	testListResources(t, lister, testInstancesData, &testSpec{index: 1})
   351  
   352  	// #################################################################
   353  	// Instances, with second NIC and public IP
   354  	// We get an error as first instance's second NIC doesn't have public IP.
   355  	// ##################################################################
   356  	testListResources(t, lister, nil, &testSpec{index: 1, ipType: "public", err: true})
   357  }
   358  
   359  func readTestJSON(t *testing.T, fileName string) (b []byte) {
   360  	t.Helper()
   361  
   362  	instancesListFile := "./testdata/" + fileName
   363  	data, err := ioutil.ReadFile(instancesListFile)
   364  
   365  	if err != nil {
   366  		t.Fatalf("error reading test data file: %s", instancesListFile)
   367  	}
   368  
   369  	return data
   370  }
   371  
   372  func TestExpand(t *testing.T) {
   373  	project, zone, apiVersion := "proj1", "us-central1-a", "v1"
   374  
   375  	testGetURL := func(_ *http.Client, url string) ([]byte, error) {
   376  		switch url {
   377  		case "https://www.googleapis.com/compute/v1/projects/proj1/zones/us-central1-a/instances?filter=status%20eq%20%22RUNNING%22":
   378  			return readTestJSON(t, "instances.json"), nil
   379  		case "https://www.googleapis.com/compute/v1/projects/proj1/zones/us-central1-b/instances?filter=status%20eq%20%22RUNNING%22":
   380  			return []byte("{\"items\": []}"), nil // No instances in us-central1-b
   381  		case "https://www.googleapis.com/compute/v1/projects/proj1/zones":
   382  			return readTestJSON(t, "zones.json"), nil
   383  		}
   384  		// Return error for non-matching URL.
   385  		return nil, fmt.Errorf("unknown url: %s", url)
   386  	}
   387  
   388  	il := &gceInstancesLister{
   389  		project:       project,
   390  		baseAPIPath:   "https://www.googleapis.com/compute/" + apiVersion + "/projects/" + project,
   391  		getURLFunc:    testGetURL,
   392  		cachePerScope: make(map[string]map[string]*instanceData),
   393  		namesPerScope: make(map[string][]string),
   394  	}
   395  
   396  	timeBeforeExpand := time.Now().Unix()
   397  
   398  	il.expand(time.Second)
   399  
   400  	var gotZones []string
   401  	for z := range il.namesPerScope {
   402  		gotZones = append(gotZones, z)
   403  	}
   404  	// Sort as zones are shuffled in expand.
   405  	sort.Strings(gotZones)
   406  	wantZones := []string{"us-central1-a", "us-central1-b"}
   407  	if !reflect.DeepEqual(gotZones, wantZones) {
   408  		t.Errorf("got zones=%v, want zones=%v", gotZones, wantZones)
   409  	}
   410  
   411  	gotNames, gotInstances := il.namesPerScope[zone], il.cachePerScope[zone]
   412  
   413  	// Expected data comes from the JSON file.
   414  	wantNames := []string{
   415  		"ig-us-central1-a-00-abcd",
   416  		"ig-us-central1-a-01-efgh",
   417  	}
   418  	wantLabels := []map[string]string{
   419  		map[string]string{"app": "cloudprober", "shard": "00"},
   420  		map[string]string{"app": "cloudprober", "shard": "01"},
   421  	}
   422  	wantNetworks := [][][4]string{
   423  		[][4]string{
   424  			{"10.0.0.2", "194.197.208.201", "2600:2d00:4030:a47:c0a8:2110:0:0", "2600:2d00:4030:a47:c0a8:2110:1:0"},
   425  			{"10.0.0.3", "194.197.208.202", "2600:2d00:4030:a47:c0a8:2110:0:1", ""},
   426  		},
   427  		[][4]string{{"10.0.1.3", "194.197.209.202", "", ""}},
   428  	}
   429  
   430  	if !reflect.DeepEqual(gotNames, wantNames) {
   431  		t.Errorf("Got names=%v, want=%v", gotNames, wantNames)
   432  	}
   433  
   434  	for i, name := range wantNames {
   435  		ins := gotInstances[name]
   436  
   437  		// Check lastupdate timestamp
   438  		if ins.lastUpdated < timeBeforeExpand {
   439  			t.Errorf("Instance's last update timestamp (%d) was not updated during expand (timestamp before expand: %d).", ins.lastUpdated, timeBeforeExpand)
   440  		}
   441  
   442  		// Check for labels
   443  		gotLabels := ins.ii.Labels
   444  		wantLabels := wantLabels[i]
   445  		if !reflect.DeepEqual(gotLabels, wantLabels) {
   446  			t.Errorf("Got labels=%v, want labels=%v", gotLabels, wantLabels)
   447  		}
   448  
   449  		// Check for ips
   450  		if len(ins.ii.NetworkInterfaces) != len(wantNetworks[i]) {
   451  			t.Errorf("Got %d nics, want %d", len(ins.ii.NetworkInterfaces), len(wantNetworks[i]))
   452  		}
   453  		for i, wantIPs := range wantNetworks[i] {
   454  			nic := ins.ii.NetworkInterfaces[i]
   455  			gotIPs := [4]string{nic.NetworkIP, nic.AccessConfigs[0].NatIP, nic.Ipv6Address}
   456  			if len(nic.Ipv6AccessConfigs) > 0 {
   457  				gotIPs[3] = nic.Ipv6AccessConfigs[0].ExternalIpv6
   458  			}
   459  			if gotIPs != wantIPs {
   460  				t.Errorf("Got ips=%v, want=%v", gotIPs, wantIPs)
   461  			}
   462  		}
   463  	}
   464  }