istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/serviceregistry/kube/controller/serviceexportcache_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 "testing" 21 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 24 "k8s.io/apimachinery/pkg/runtime/schema" 25 "k8s.io/apimachinery/pkg/types" 26 mcsapi "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" 27 28 "istio.io/istio/pilot/pkg/features" 29 "istio.io/istio/pilot/pkg/model" 30 "istio.io/istio/pilot/pkg/serviceregistry/kube" 31 "istio.io/istio/pilot/pkg/serviceregistry/util/xdsfake" 32 "istio.io/istio/pkg/config/host" 33 "istio.io/istio/pkg/kube/mcs" 34 "istio.io/istio/pkg/maps" 35 "istio.io/istio/pkg/slices" 36 istiotest "istio.io/istio/pkg/test" 37 "istio.io/istio/pkg/test/util/retry" 38 ) 39 40 const ( 41 serviceExportName = "test-svc" 42 serviceExportNamespace = "test-ns" 43 serviceExportPodIP = "128.0.0.2" 44 testCluster = "test-cluster" 45 ) 46 47 var serviceExportNamespacedName = types.NamespacedName{ 48 Namespace: serviceExportNamespace, 49 Name: serviceExportName, 50 } 51 52 type ClusterLocalMode string 53 54 func (m ClusterLocalMode) String() string { 55 return string(m) 56 } 57 58 const ( 59 alwaysClusterLocal ClusterLocalMode = "always cluster local" 60 meshWide ClusterLocalMode = "mesh wide" 61 ) 62 63 var ClusterLocalModes = []ClusterLocalMode{alwaysClusterLocal, meshWide} 64 65 func TestServiceNotExported(t *testing.T) { 66 for _, clusterLocalMode := range ClusterLocalModes { 67 t.Run(clusterLocalMode.String(), func(t *testing.T) { 68 // Create and run the controller. 69 ec, endpoints := newTestServiceExportCache(t, clusterLocalMode) 70 // Check that the endpoint is cluster-local 71 ec.checkServiceInstancesOrFail(t, false, endpoints) 72 }) 73 } 74 } 75 76 func TestServiceExported(t *testing.T) { 77 for _, clusterLocalMode := range ClusterLocalModes { 78 t.Run(clusterLocalMode.String(), func(t *testing.T) { 79 // Create and run the controller. 80 ec, endpoints := newTestServiceExportCache(t, clusterLocalMode) 81 // Export the service. 82 ec.export(t) 83 84 // Check that the endpoint is mesh-wide 85 ec.checkServiceInstancesOrFail(t, true, endpoints) 86 }) 87 } 88 } 89 90 func TestServiceUnexported(t *testing.T) { 91 for _, clusterLocalMode := range ClusterLocalModes { 92 t.Run(clusterLocalMode.String(), func(t *testing.T) { 93 // Create and run the controller. 94 ec, endpoints := newTestServiceExportCache(t, clusterLocalMode) 95 // Export the service and then unexport it immediately. 96 ec.export(t) 97 ec.unExport(t) 98 99 // Check that the endpoint is cluster-local 100 ec.checkServiceInstancesOrFail(t, false, endpoints) 101 }) 102 } 103 } 104 105 func newServiceExport() *unstructured.Unstructured { 106 se := &mcsapi.ServiceExport{ 107 TypeMeta: metav1.TypeMeta{ 108 Kind: "ServiceExport", 109 APIVersion: mcs.MCSSchemeGroupVersion.String(), 110 }, 111 ObjectMeta: metav1.ObjectMeta{ 112 Name: serviceExportName, 113 Namespace: serviceExportNamespace, 114 }, 115 } 116 return toUnstructured(se) 117 } 118 119 func newTestServiceExportCache(t *testing.T, clusterLocalMode ClusterLocalMode) (*serviceExportCacheImpl, *model.EndpointIndex) { 120 t.Helper() 121 122 istiotest.SetForTest(t, &features.EnableMCSServiceDiscovery, true) 123 istiotest.SetForTest(t, &features.EnableMCSClusterLocal, clusterLocalMode == alwaysClusterLocal) 124 125 c, _ := NewFakeControllerWithOptions(t, FakeControllerOptions{ 126 ClusterID: testCluster, 127 CRDs: []schema.GroupVersionResource{mcs.ServiceExportGVR}, 128 }) 129 130 // Create the test service and endpoints. 131 createService(c, serviceExportName, serviceExportNamespace, map[string]string{}, map[string]string{}, 132 []int32{8080}, map[string]string{"app": "prod-app"}, t) 133 createEndpoints(t, c, serviceExportName, serviceExportNamespace, []string{"tcp-port"}, []string{serviceExportPodIP}, nil, nil) 134 135 ec := c.exports.(*serviceExportCacheImpl) 136 // Wait for the resources to be processed by the controller. 137 retry.UntilOrFail(t, func() bool { 138 if svc := ec.GetService(ec.serviceHostname()); svc == nil { 139 return false 140 } 141 inst := ec.getEndpoint(c.Endpoints) 142 return inst != nil 143 }, serviceExportTimeout) 144 return ec, c.Endpoints 145 } 146 147 func (ec *serviceExportCacheImpl) serviceHostname() host.Name { 148 return kube.ServiceHostname(serviceExportName, serviceExportNamespace, ec.opts.DomainSuffix) 149 } 150 151 func (ec *serviceExportCacheImpl) export(t *testing.T) { 152 t.Helper() 153 154 _, err := ec.client.Dynamic().Resource(mcs.ServiceExportGVR).Namespace(serviceExportNamespace).Create(context.TODO(), 155 newServiceExport(), 156 metav1.CreateOptions{}) 157 if err != nil { 158 t.Fatal(err) 159 } 160 161 // Wait for the export to be processed by the controller. 162 retry.UntilOrFail(t, func() bool { 163 return ec.isExported(serviceExportNamespacedName) 164 }, serviceExportTimeout, retry.Message("expected to be exported")) 165 166 // Wait for the XDS event. 167 ec.waitForXDS(t, true) 168 } 169 170 func (ec *serviceExportCacheImpl) unExport(t *testing.T) { 171 t.Helper() 172 173 _ = ec.client.Dynamic().Resource(mcs.ServiceExportGVR).Namespace(serviceExportNamespace).Delete( 174 context.TODO(), 175 serviceExportName, 176 metav1.DeleteOptions{}) 177 178 // Wait for the delete to be processed by the controller. 179 retry.UntilOrFail(t, func() bool { 180 return !ec.isExported(serviceExportNamespacedName) 181 }, serviceExportTimeout) 182 183 // Wait for the XDS event. 184 ec.waitForXDS(t, false) 185 } 186 187 func (ec *serviceExportCacheImpl) waitForXDS(t *testing.T, exported bool) { 188 t.Helper() 189 retry.UntilSuccessOrFail(t, func() error { 190 event := ec.opts.XDSUpdater.(*xdsfake.Updater).WaitOrFail(t, "eds") 191 if len(event.Endpoints) != 1 { 192 return fmt.Errorf("waitForXDS failed: expected 1 endpoint, found %d", len(event.Endpoints)) 193 } 194 195 hostName := host.Name(event.ID) 196 svc := ec.GetService(hostName) 197 if svc == nil { 198 return fmt.Errorf("unable to find service for host %s", hostName) 199 } 200 return ec.checkEndpoint(exported, event.Endpoints[0]) 201 }, serviceExportTimeout) 202 } 203 204 func (ec *serviceExportCacheImpl) getEndpoint(endpoints *model.EndpointIndex) *model.IstioEndpoint { 205 svcs := ec.Services() 206 for _, s := range svcs { 207 ep := GetEndpoints(s, endpoints) 208 if len(ep) > 0 { 209 return ep[0] 210 } 211 } 212 return nil 213 } 214 215 func GetEndpoints(s *model.Service, endpoints *model.EndpointIndex) []*model.IstioEndpoint { 216 return GetEndpointsForPort(s, endpoints, 0) 217 } 218 219 func GetEndpointsForPort(s *model.Service, endpoints *model.EndpointIndex, port int) []*model.IstioEndpoint { 220 shards, ok := endpoints.ShardsForService(string(s.Hostname), s.Attributes.Namespace) 221 if !ok { 222 return nil 223 } 224 var pn string 225 for _, p := range s.Ports { 226 if p.Port == port { 227 pn = p.Name 228 break 229 } 230 } 231 if pn == "" && port != 0 { 232 return nil 233 } 234 shards.RLock() 235 defer shards.RUnlock() 236 return slices.FilterInPlace(slices.Flatten(maps.Values(shards.Shards)), func(endpoint *model.IstioEndpoint) bool { 237 return pn == "" || endpoint.ServicePortName == pn 238 }) 239 } 240 241 func (ec *serviceExportCacheImpl) checkServiceInstancesOrFail(t *testing.T, exported bool, endpoints *model.EndpointIndex) { 242 t.Helper() 243 if err := ec.checkEndpoints(exported, endpoints); err != nil { 244 t.Fatal(err) 245 } 246 } 247 248 func (ec *serviceExportCacheImpl) checkEndpoints(exported bool, endpoints *model.EndpointIndex) error { 249 ep := ec.getEndpoint(endpoints) 250 if ep == nil { 251 return fmt.Errorf("expected an endpoint, found none") 252 } 253 return ec.checkEndpoint(exported, ep) 254 } 255 256 func (ec *serviceExportCacheImpl) checkEndpoint(exported bool, ep *model.IstioEndpoint) error { 257 // Should always be discoverable from the same cluster. 258 if err := ec.checkDiscoverableFromSameCluster(ep); err != nil { 259 return err 260 } 261 262 if exported && !features.EnableMCSClusterLocal { 263 return ec.checkDiscoverableFromDifferentCluster(ep) 264 } 265 266 return ec.checkNotDiscoverableFromDifferentCluster(ep) 267 } 268 269 func (ec *serviceExportCacheImpl) checkDiscoverableFromSameCluster(ep *model.IstioEndpoint) error { 270 if !ec.isDiscoverableFromSameCluster(ep) { 271 return fmt.Errorf("endpoint was not discoverable from the same cluster") 272 } 273 return nil 274 } 275 276 func (ec *serviceExportCacheImpl) checkDiscoverableFromDifferentCluster(ep *model.IstioEndpoint) error { 277 if !ec.isDiscoverableFromDifferentCluster(ep) { 278 return fmt.Errorf("endpoint was not discoverable from a different cluster") 279 } 280 return nil 281 } 282 283 func (ec *serviceExportCacheImpl) checkNotDiscoverableFromDifferentCluster(ep *model.IstioEndpoint) error { 284 if ec.isDiscoverableFromDifferentCluster(ep) { 285 return fmt.Errorf("endpoint was discoverable from a different cluster") 286 } 287 return nil 288 } 289 290 func (ec *serviceExportCacheImpl) isDiscoverableFromSameCluster(ep *model.IstioEndpoint) bool { 291 return ep.IsDiscoverableFromProxy(&model.Proxy{ 292 Metadata: &model.NodeMetadata{ 293 ClusterID: ec.Cluster(), 294 }, 295 }) 296 } 297 298 func (ec *serviceExportCacheImpl) isDiscoverableFromDifferentCluster(ep *model.IstioEndpoint) bool { 299 return ep.IsDiscoverableFromProxy(&model.Proxy{ 300 Metadata: &model.NodeMetadata{ 301 ClusterID: "some-other-cluster", 302 }, 303 }) 304 }