istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/serviceimportcache_test.go (about)

     1  // Copyright Istio 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 controller
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  	"testing"
    22  	"time"
    23  
    24  	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	"k8s.io/apimachinery/pkg/types"
    30  	mcsapi "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1"
    31  
    32  	"istio.io/api/label"
    33  	"istio.io/istio/pilot/pkg/features"
    34  	"istio.io/istio/pilot/pkg/model"
    35  	"istio.io/istio/pilot/pkg/serviceregistry/kube"
    36  	"istio.io/istio/pilot/pkg/serviceregistry/util/xdsfake"
    37  	"istio.io/istio/pkg/config/host"
    38  	"istio.io/istio/pkg/kube/mcs"
    39  	"istio.io/istio/pkg/test"
    40  	"istio.io/istio/pkg/test/util/assert"
    41  	"istio.io/istio/pkg/test/util/retry"
    42  )
    43  
    44  const (
    45  	serviceImportName      = "test-svc"
    46  	serviceImportNamespace = "test-ns"
    47  	serviceImportPodIP     = "128.0.0.2"
    48  	serviceImportCluster   = "test-cluster"
    49  )
    50  
    51  var (
    52  	serviceImportNamespacedName = types.NamespacedName{
    53  		Namespace: serviceImportNamespace,
    54  		Name:      serviceImportName,
    55  	}
    56  	serviceImportClusterSetHost = serviceClusterSetLocalHostname(serviceImportNamespacedName)
    57  	serviceImportVIPs           = []string{"1.1.1.1"}
    58  	serviceImportTimeout        = retry.Timeout(2 * time.Second)
    59  )
    60  
    61  func TestServiceNotImported(t *testing.T) {
    62  	c, ic := newTestServiceImportCache(t)
    63  	ic.createKubeService(t, c)
    64  
    65  	// Check that the service does not have ClusterSet IPs.
    66  	ic.checkServiceInstances(t)
    67  }
    68  
    69  func TestServiceImportedAfterCreated(t *testing.T) {
    70  	c, ic := newTestServiceImportCache(t)
    71  
    72  	ic.createKubeService(t, c)
    73  	ic.createServiceImport(t, mcsapi.ClusterSetIP, serviceImportVIPs)
    74  
    75  	// Check that the service has been assigned ClusterSet IPs.
    76  	ic.checkServiceInstances(t)
    77  }
    78  
    79  func TestServiceCreatedAfterImported(t *testing.T) {
    80  	c, ic := newTestServiceImportCache(t)
    81  
    82  	ic.createServiceImport(t, mcsapi.ClusterSetIP, serviceImportVIPs)
    83  	ic.createKubeService(t, c)
    84  
    85  	// Check that the service has been assigned ClusterSet IPs.
    86  	ic.checkServiceInstances(t)
    87  }
    88  
    89  func TestUpdateImportedService(t *testing.T) {
    90  	c, ic := newTestServiceImportCache(t)
    91  
    92  	ic.createKubeService(t, c)
    93  	ic.createServiceImport(t, mcsapi.ClusterSetIP, serviceImportVIPs)
    94  	ic.checkServiceInstances(t)
    95  
    96  	// Update the k8s service and verify that both services are updated.
    97  	ic.updateKubeService(t)
    98  }
    99  
   100  func TestHeadlessServiceImported(t *testing.T) {
   101  	// Create and run the controller.
   102  	c, ic := newTestServiceImportCache(t)
   103  
   104  	ic.createKubeService(t, c)
   105  	ic.createServiceImport(t, mcsapi.Headless, nil)
   106  
   107  	// Verify that we did not generate the synthetic service for the headless service.
   108  	ic.checkServiceInstances(t)
   109  }
   110  
   111  func TestDeleteImportedService(t *testing.T) {
   112  	// Create and run the controller.
   113  	c1, ic := newTestServiceImportCache(t)
   114  
   115  	// Create and run another controller.
   116  	c2, _ := NewFakeControllerWithOptions(t, FakeControllerOptions{
   117  		ClusterID: "test-cluster2",
   118  	})
   119  
   120  	c1.opts.MeshServiceController.AddRegistryAndRun(c2, c2.stop)
   121  
   122  	ic.createKubeService(t, c1)
   123  	ic.createServiceImport(t, mcsapi.ClusterSetIP, serviceImportVIPs)
   124  	ic.checkServiceInstances(t)
   125  
   126  	// create the same service in cluster2
   127  	createService(c2, serviceImportName, serviceImportNamespace, map[string]string{}, map[string]string{},
   128  		[]int32{8080}, map[string]string{"app": "prod-app"}, t)
   129  
   130  	// Delete the k8s service and verify that all internal services are removed.
   131  	ic.deleteKubeService(t, c2)
   132  }
   133  
   134  func TestUnimportService(t *testing.T) {
   135  	// Create and run the controller.
   136  	c, ic := newTestServiceImportCache(t)
   137  
   138  	ic.createKubeService(t, c)
   139  	ic.createServiceImport(t, mcsapi.ClusterSetIP, serviceImportVIPs)
   140  	ic.checkServiceInstances(t)
   141  
   142  	ic.unimportService(t)
   143  }
   144  
   145  func TestAddServiceImportVIPs(t *testing.T) {
   146  	// Create and run the controller.
   147  	c, ic := newTestServiceImportCache(t)
   148  
   149  	ic.createKubeService(t, c)
   150  	ic.createServiceImport(t, mcsapi.ClusterSetIP, nil)
   151  	ic.checkServiceInstances(t)
   152  
   153  	ic.setServiceImportVIPs(t, serviceImportVIPs)
   154  }
   155  
   156  func TestUpdateServiceImportVIPs(t *testing.T) {
   157  	// Create and run the controller.
   158  	c, ic := newTestServiceImportCache(t)
   159  
   160  	ic.createKubeService(t, c)
   161  	ic.createServiceImport(t, mcsapi.ClusterSetIP, serviceImportVIPs)
   162  	ic.checkServiceInstances(t)
   163  
   164  	updatedVIPs := []string{"1.1.1.1", "1.1.1.2"}
   165  	ic.setServiceImportVIPs(t, updatedVIPs)
   166  }
   167  
   168  func newTestServiceImportCache(t test.Failer) (*FakeController, *serviceImportCacheImpl) {
   169  	test.SetForTest(t, &features.EnableMCSHost, true)
   170  
   171  	c, _ := NewFakeControllerWithOptions(t, FakeControllerOptions{
   172  		ClusterID: serviceImportCluster,
   173  		CRDs:      []schema.GroupVersionResource{mcs.ServiceImportGVR},
   174  	})
   175  
   176  	return c, c.imports.(*serviceImportCacheImpl)
   177  }
   178  
   179  func (ic *serviceImportCacheImpl) createKubeService(t *testing.T, c *FakeController) {
   180  	t.Helper()
   181  
   182  	// Create the test service and endpoints.
   183  	createService(c, serviceImportName, serviceImportNamespace, map[string]string{}, map[string]string{},
   184  		[]int32{8080}, map[string]string{"app": "prod-app"}, t)
   185  	createEndpoints(t, c, serviceImportName, serviceImportNamespace, []string{"tcp-port"}, []string{serviceImportPodIP}, nil, nil)
   186  
   187  	isImported := ic.isImported(serviceImportNamespacedName)
   188  
   189  	// Wait for the resources to be processed by the controller.
   190  	retry.UntilSuccessOrFail(t, func() error {
   191  		clusterLocalHost := ic.clusterLocalHost()
   192  		if svc := c.GetService(clusterLocalHost); svc == nil {
   193  			return fmt.Errorf("failed looking up service for host %s", clusterLocalHost)
   194  		}
   195  
   196  		var expectedHosts map[host.Name]struct{}
   197  		if isImported {
   198  			expectedHosts = map[host.Name]struct{}{
   199  				clusterLocalHost:            {},
   200  				serviceImportClusterSetHost: {},
   201  			}
   202  		} else {
   203  			expectedHosts = map[host.Name]struct{}{
   204  				clusterLocalHost: {},
   205  			}
   206  		}
   207  
   208  		instances := ic.getProxyServiceTargets()
   209  		if len(instances) != len(expectedHosts) {
   210  			return fmt.Errorf("expected 1 service instance, found %d", len(instances))
   211  		}
   212  		for _, si := range instances {
   213  			if si.Service == nil {
   214  				return fmt.Errorf("proxy ServiceInstance has nil service")
   215  			}
   216  			if _, found := expectedHosts[si.Service.Hostname]; !found {
   217  				return fmt.Errorf("found proxy ServiceInstance for unexpected host: %s", si.Service.Hostname)
   218  			}
   219  			delete(expectedHosts, si.Service.Hostname)
   220  		}
   221  
   222  		if len(expectedHosts) > 0 {
   223  			return fmt.Errorf("failed to find proxy ServiceEndpoints for hosts: %v", expectedHosts)
   224  		}
   225  
   226  		return nil
   227  	}, serviceImportTimeout)
   228  }
   229  
   230  func (ic *serviceImportCacheImpl) updateKubeService(t *testing.T) {
   231  	t.Helper()
   232  	svc, _ := ic.client.Kube().CoreV1().Services(serviceImportNamespace).Get(context.TODO(), serviceImportName, metav1.GetOptions{})
   233  	if svc == nil {
   234  		t.Fatalf("failed to find k8s service: %s/%s", serviceImportNamespace, serviceImportName)
   235  	}
   236  
   237  	// Just add a new label.
   238  	svc.Labels = map[string]string{
   239  		"foo": "bar",
   240  	}
   241  	if _, err := ic.client.Kube().CoreV1().Services(serviceImportNamespace).Update(context.TODO(), svc, metav1.UpdateOptions{}); err != nil {
   242  		t.Fatal(err)
   243  	}
   244  
   245  	hostNames := []host.Name{
   246  		ic.clusterLocalHost(),
   247  		serviceImportClusterSetHost,
   248  	}
   249  
   250  	// Wait for the services to pick up the label.
   251  	retry.UntilSuccessOrFail(t, func() error {
   252  		for _, hostName := range hostNames {
   253  			svc := ic.GetService(hostName)
   254  			if svc == nil {
   255  				return fmt.Errorf("failed to find service for host %s", hostName)
   256  			}
   257  			if svc.Attributes.Labels["foo"] != "bar" {
   258  				return fmt.Errorf("service not updated for %s", hostName)
   259  			}
   260  		}
   261  
   262  		return nil
   263  	}, serviceImportTimeout)
   264  }
   265  
   266  func (ic *serviceImportCacheImpl) deleteKubeService(t *testing.T, anotherCluster *FakeController) {
   267  	t.Helper()
   268  
   269  	if err := anotherCluster.client.Kube().
   270  		CoreV1().Services(serviceImportNamespace).Delete(context.TODO(), serviceImportName, metav1.DeleteOptions{}); err != nil {
   271  		t.Fatal(err)
   272  	}
   273  	// Wait for the resources to be processed by the controller.
   274  	if err := ic.client.Kube().CoreV1().Services(serviceImportNamespace).Delete(context.TODO(), serviceImportName, metav1.DeleteOptions{}); err != nil {
   275  		t.Fatal(err)
   276  	}
   277  
   278  	// Wait for the resources to be processed by the controller.
   279  	retry.UntilSuccessOrFail(t, func() error {
   280  		if svc := ic.GetService(ic.clusterLocalHost()); svc != nil {
   281  			return fmt.Errorf("found deleted service for host %s", ic.clusterLocalHost())
   282  		}
   283  		if svc := ic.GetService(serviceImportClusterSetHost); svc != nil {
   284  			return fmt.Errorf("found deleted service for host %s", serviceImportClusterSetHost)
   285  		}
   286  
   287  		instances := ic.getProxyServiceTargets()
   288  		if len(instances) != 0 {
   289  			return fmt.Errorf("expected 0 service instance, found %d", len(instances))
   290  		}
   291  
   292  		return nil
   293  	}, serviceImportTimeout)
   294  }
   295  
   296  func (ic *serviceImportCacheImpl) getProxyServiceTargets() []model.ServiceTarget {
   297  	return ic.GetProxyServiceTargets(&model.Proxy{
   298  		Type:            model.SidecarProxy,
   299  		IPAddresses:     []string{serviceImportPodIP},
   300  		Locality:        &core.Locality{Region: "r", Zone: "z"},
   301  		ConfigNamespace: serviceImportNamespace,
   302  		Labels: map[string]string{
   303  			"app":                      "prod-app",
   304  			label.SecurityTlsMode.Name: "mutual",
   305  		},
   306  		Metadata: &model.NodeMetadata{
   307  			ServiceAccount: "account",
   308  			ClusterID:      ic.Cluster(),
   309  			Labels: map[string]string{
   310  				"app":                      "prod-app",
   311  				label.SecurityTlsMode.Name: "mutual",
   312  			},
   313  		},
   314  	})
   315  }
   316  
   317  func (ic *serviceImportCacheImpl) getServiceImport(t *testing.T) *mcsapi.ServiceImport {
   318  	t.Helper()
   319  
   320  	// Get the ServiceImport as unstructured
   321  	u, err := ic.client.Dynamic().Resource(mcs.ServiceImportGVR).Namespace(serviceImportNamespace).Get(
   322  		context.TODO(), serviceImportName, metav1.GetOptions{})
   323  	if err != nil {
   324  		return nil
   325  	}
   326  
   327  	// Convert to ServiceImport
   328  	si := &mcsapi.ServiceImport{}
   329  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, si); err != nil {
   330  		t.Fatal(err)
   331  	}
   332  	return si
   333  }
   334  
   335  func (ic *serviceImportCacheImpl) checkServiceInstances(t *testing.T) {
   336  	t.Helper()
   337  
   338  	si := ic.getServiceImport(t)
   339  
   340  	var expectedIPs []string
   341  	expectedServiceCount := 1
   342  	expectMCSService := false
   343  	if si != nil && si.Spec.Type == mcsapi.ClusterSetIP && len(si.Spec.IPs) > 0 {
   344  		expectedIPs = si.Spec.IPs
   345  		expectedServiceCount = 2
   346  		expectMCSService = true
   347  	}
   348  
   349  	instances := ic.getProxyServiceTargets()
   350  	assert.Equal(t, len(instances), expectedServiceCount)
   351  
   352  	for _, inst := range instances {
   353  		svc := inst.Service
   354  		if svc.Hostname == serviceImportClusterSetHost {
   355  			if !expectMCSService {
   356  				t.Fatalf("found ServiceInstance for unimported service %s", serviceImportClusterSetHost)
   357  			}
   358  			// Check the ClusterSet IPs.
   359  			assert.Equal(t, svc.ClusterVIPs.GetAddressesFor(ic.Cluster()), expectedIPs)
   360  			return
   361  		}
   362  	}
   363  
   364  	if expectMCSService {
   365  		t.Fatalf("failed finding ServiceInstance for %s", serviceImportClusterSetHost)
   366  	}
   367  }
   368  
   369  func (ic *serviceImportCacheImpl) createServiceImport(t *testing.T, importType mcsapi.ServiceImportType, vips []string) {
   370  	t.Helper()
   371  
   372  	// Create the ServiceImport resource in the cluster.
   373  	_, err := ic.client.Dynamic().Resource(mcs.ServiceImportGVR).Namespace(serviceImportNamespace).Create(context.TODO(),
   374  		newServiceImport(importType, vips),
   375  		metav1.CreateOptions{})
   376  	if err != nil {
   377  		t.Fatal(err)
   378  	}
   379  
   380  	shouldCreateMCSService := importType == mcsapi.ClusterSetIP && len(vips) > 0 &&
   381  		ic.GetService(ic.clusterLocalHost()) != nil
   382  
   383  	// Wait for the import to be processed by the controller.
   384  	retry.UntilSuccessOrFail(t, func() error {
   385  		if !ic.isImported(serviceImportNamespacedName) {
   386  			return fmt.Errorf("serviceImport not found for %s", serviceImportClusterSetHost)
   387  		}
   388  		if shouldCreateMCSService && ic.GetService(serviceImportClusterSetHost) == nil {
   389  			return fmt.Errorf("failed to find service for %s", serviceImportClusterSetHost)
   390  		}
   391  		return nil
   392  	}, serviceImportTimeout)
   393  
   394  	if shouldCreateMCSService {
   395  		// Wait for the XDS event.
   396  		ic.checkXDS(t)
   397  	}
   398  }
   399  
   400  func (ic *serviceImportCacheImpl) setServiceImportVIPs(t *testing.T, vips []string) {
   401  	t.Helper()
   402  
   403  	// Get the ServiceImport
   404  	si := ic.getServiceImport(t)
   405  
   406  	// Apply the ClusterSet IPs.
   407  	si.Spec.IPs = vips
   408  	if _, err := ic.client.Dynamic().Resource(mcs.ServiceImportGVR).Namespace(serviceImportNamespace).Update(
   409  		context.TODO(), toUnstructured(si), metav1.UpdateOptions{}); err != nil {
   410  		t.Fatal(err)
   411  	}
   412  
   413  	if len(vips) > 0 {
   414  		// Wait for the import to be processed by the controller.
   415  		retry.UntilSuccessOrFail(t, func() error {
   416  			svc := ic.GetService(serviceImportClusterSetHost)
   417  			if svc == nil {
   418  				return fmt.Errorf("failed to find service for %s", serviceImportClusterSetHost)
   419  			}
   420  
   421  			actualVIPs := svc.ClusterVIPs.GetAddressesFor(ic.Cluster())
   422  			if !reflect.DeepEqual(vips, actualVIPs) {
   423  				return fmt.Errorf("expected ClusterSet VIPs %v, but found %v", vips, actualVIPs)
   424  			}
   425  			return nil
   426  		}, serviceImportTimeout)
   427  
   428  		// Wait for the XDS event.
   429  		ic.checkXDS(t)
   430  	} else {
   431  		// Wait for the import to be processed by the controller.
   432  		retry.UntilSuccessOrFail(t, func() error {
   433  			if svc := ic.GetService(serviceImportClusterSetHost); svc != nil {
   434  				return fmt.Errorf("found unexpected service for %s", serviceImportClusterSetHost)
   435  			}
   436  			return nil
   437  		}, serviceImportTimeout)
   438  	}
   439  }
   440  
   441  func (ic *serviceImportCacheImpl) unimportService(t *testing.T) {
   442  	t.Helper()
   443  
   444  	if err := ic.client.Dynamic().Resource(mcs.ServiceImportGVR).Namespace(serviceImportNamespace).Delete(
   445  		context.TODO(), serviceImportName, metav1.DeleteOptions{}); err != nil {
   446  		t.Fatal(err)
   447  	}
   448  
   449  	// Wait for the import to be processed by the controller.
   450  	retry.UntilSuccessOrFail(t, func() error {
   451  		if ic.isImported(serviceImportNamespacedName) {
   452  			return fmt.Errorf("serviceImport found for %s", serviceImportClusterSetHost)
   453  		}
   454  		if ic.GetService(serviceImportClusterSetHost) != nil {
   455  			return fmt.Errorf("found MCS service for unimported service %s", serviceImportClusterSetHost)
   456  		}
   457  		return nil
   458  	}, serviceImportTimeout)
   459  }
   460  
   461  func (ic *serviceImportCacheImpl) isImported(name types.NamespacedName) bool {
   462  	return ic.serviceImports.Get(name.Name, name.Namespace) != nil
   463  }
   464  
   465  func (ic *serviceImportCacheImpl) checkXDS(t test.Failer) {
   466  	t.Helper()
   467  	ic.opts.XDSUpdater.(*xdsfake.Updater).MatchOrFail(t, xdsfake.Event{Type: "service", ID: serviceImportClusterSetHost.String()})
   468  }
   469  
   470  func (ic *serviceImportCacheImpl) clusterLocalHost() host.Name {
   471  	return kube.ServiceHostname(serviceImportName, serviceImportNamespace, ic.opts.DomainSuffix)
   472  }
   473  
   474  func newServiceImport(importType mcsapi.ServiceImportType, vips []string) *unstructured.Unstructured {
   475  	si := &mcsapi.ServiceImport{
   476  		TypeMeta: metav1.TypeMeta{
   477  			Kind:       "ServiceImport",
   478  			APIVersion: "multicluster.x-k8s.io/v1alpha1",
   479  		},
   480  		ObjectMeta: metav1.ObjectMeta{
   481  			Name:      serviceImportName,
   482  			Namespace: serviceImportNamespace,
   483  		},
   484  		Spec: mcsapi.ServiceImportSpec{
   485  			Type: importType,
   486  			IPs:  vips,
   487  		},
   488  	}
   489  	return toUnstructured(si)
   490  }
   491  
   492  func toUnstructured(o any) *unstructured.Unstructured {
   493  	u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
   494  	if err != nil {
   495  		panic(err)
   496  	}
   497  	return &unstructured.Unstructured{Object: u}
   498  }