1// Copyright 2017 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 15package kube 16 17import ( 18 "fmt" 19 "reflect" 20 "strings" 21 "testing" 22 "time" 23 24 "istio.io/api/annotation" 25 "istio.io/istio/pilot/pkg/model" 26 "istio.io/istio/pkg/config/kube" 27 "istio.io/istio/pkg/config/protocol" 28 "istio.io/istio/pkg/spiffe" 29 30 coreV1 "k8s.io/api/core/v1" 31 metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/util/intstr" 33) 34 35var ( 36 domainSuffix = "company.com" 37 clusterID = "test-cluster" 38) 39 40func TestConvertProtocol(t *testing.T) { 41 http := "http" 42 type protocolCase struct { 43 port int32 44 name string 45 appProtocol *string 46 proto coreV1.Protocol 47 out protocol.Instance 48 } 49 protocols := []protocolCase{ 50 {8888, "", nil, coreV1.ProtocolTCP, protocol.Unsupported}, 51 {25, "", nil, coreV1.ProtocolTCP, protocol.TCP}, 52 {53, "", nil, coreV1.ProtocolTCP, protocol.TCP}, 53 {3306, "", nil, coreV1.ProtocolTCP, protocol.TCP}, 54 {27017, "", nil, coreV1.ProtocolTCP, protocol.TCP}, 55 {8888, "http", nil, coreV1.ProtocolTCP, protocol.HTTP}, 56 {8888, "http-test", nil, coreV1.ProtocolTCP, protocol.HTTP}, 57 {8888, "http", nil, coreV1.ProtocolUDP, protocol.UDP}, 58 {8888, "httptest", nil, coreV1.ProtocolTCP, protocol.Unsupported}, 59 {25, "httptest", nil, coreV1.ProtocolTCP, protocol.TCP}, 60 {53, "httptest", nil, coreV1.ProtocolTCP, protocol.TCP}, 61 {3306, "httptest", nil, coreV1.ProtocolTCP, protocol.TCP}, 62 {27017, "httptest", nil, coreV1.ProtocolTCP, protocol.TCP}, 63 {8888, "https", nil, coreV1.ProtocolTCP, protocol.HTTPS}, 64 {8888, "https-test", nil, coreV1.ProtocolTCP, protocol.HTTPS}, 65 {8888, "http2", nil, coreV1.ProtocolTCP, protocol.HTTP2}, 66 {8888, "http2-test", nil, coreV1.ProtocolTCP, protocol.HTTP2}, 67 {8888, "grpc", nil, coreV1.ProtocolTCP, protocol.GRPC}, 68 {8888, "grpc-test", nil, coreV1.ProtocolTCP, protocol.GRPC}, 69 {8888, "grpc-web", nil, coreV1.ProtocolTCP, protocol.GRPCWeb}, 70 {8888, "grpc-web-test", nil, coreV1.ProtocolTCP, protocol.GRPCWeb}, 71 {8888, "mongo", nil, coreV1.ProtocolTCP, protocol.Mongo}, 72 {8888, "mongo-test", nil, coreV1.ProtocolTCP, protocol.Mongo}, 73 {8888, "redis", nil, coreV1.ProtocolTCP, protocol.Redis}, 74 {8888, "redis-test", nil, coreV1.ProtocolTCP, protocol.Redis}, 75 {8888, "mysql", nil, coreV1.ProtocolTCP, protocol.MySQL}, 76 {8888, "mysql-test", nil, coreV1.ProtocolTCP, protocol.MySQL}, 77 {8888, "tcp", &http, coreV1.ProtocolTCP, protocol.HTTP}, 78 } 79 80 // Create the list of cases for all of the names in both upper and lowercase. 81 cases := make([]protocolCase, 0, len(protocols)*2) 82 for _, p := range protocols { 83 name := p.name 84 85 p.name = strings.ToLower(name) 86 cases = append(cases, p) 87 88 // Don't bother adding uppercase version for empty string. 89 if name != "" { 90 p.name = strings.ToUpper(name) 91 cases = append(cases, p) 92 } 93 } 94 95 for _, c := range cases { 96 testName := strings.Replace(fmt.Sprintf("%s_%s_%d", c.name, c.proto, c.port), "-", "_", -1) 97 t.Run(testName, func(t *testing.T) { 98 out := kube.ConvertProtocol(c.port, c.name, c.proto, c.appProtocol) 99 if out != c.out { 100 t.Fatalf("convertProtocol(%d, %q, %q) => %q, want %q", c.port, c.name, c.proto, out, c.out) 101 } 102 }) 103 } 104} 105 106func BenchmarkConvertProtocol(b *testing.B) { 107 cases := []struct { 108 name string 109 proto coreV1.Protocol 110 out protocol.Instance 111 }{ 112 {"grpc-web-lowercase", coreV1.ProtocolTCP, protocol.GRPCWeb}, 113 {"GRPC-WEB-mixedcase", coreV1.ProtocolTCP, protocol.GRPCWeb}, 114 {"https-lowercase", coreV1.ProtocolTCP, protocol.HTTPS}, 115 {"HTTPS-mixedcase", coreV1.ProtocolTCP, protocol.HTTPS}, 116 } 117 118 for _, c := range cases { 119 testName := strings.Replace(c.name, "-", "_", -1) 120 b.Run(testName, func(b *testing.B) { 121 for i := 0; i < b.N; i++ { 122 out := kube.ConvertProtocol(8888, c.name, c.proto, nil) 123 if out != c.out { 124 b.Fatalf("convertProtocol(%q, %q) => %q, want %q", c.name, c.proto, out, c.out) 125 } 126 } 127 }) 128 } 129} 130 131func TestServiceConversion(t *testing.T) { 132 serviceName := "service1" 133 namespace := "default" 134 saA := "serviceaccountA" 135 saB := "serviceaccountB" 136 saC := "spiffe://accounts.google.com/serviceaccountC@cloudservices.gserviceaccount.com" 137 saD := "spiffe://accounts.google.com/serviceaccountD@developer.gserviceaccount.com" 138 139 oldTrustDomain := spiffe.GetTrustDomain() 140 spiffe.SetTrustDomain(domainSuffix) 141 defer spiffe.SetTrustDomain(oldTrustDomain) 142 143 ip := "10.0.0.1" 144 145 tnow := time.Now() 146 localSvc := coreV1.Service{ 147 ObjectMeta: metaV1.ObjectMeta{ 148 Name: serviceName, 149 Namespace: namespace, 150 Annotations: map[string]string{ 151 annotation.AlphaKubernetesServiceAccounts.Name: saA + "," + saB, 152 annotation.AlphaCanonicalServiceAccounts.Name: saC + "," + saD, 153 "other/annotation": "test", 154 }, 155 CreationTimestamp: metaV1.Time{Time: tnow}, 156 }, 157 Spec: coreV1.ServiceSpec{ 158 ClusterIP: ip, 159 Ports: []coreV1.ServicePort{ 160 { 161 Name: "http", 162 Port: 8080, 163 Protocol: coreV1.ProtocolTCP, 164 }, 165 { 166 Name: "https", 167 Protocol: coreV1.ProtocolTCP, 168 Port: 443, 169 }, 170 }, 171 }, 172 } 173 174 service := ConvertService(localSvc, domainSuffix, clusterID) 175 if service == nil { 176 t.Fatalf("could not convert service") 177 } 178 179 if service.CreationTime != tnow { 180 t.Fatalf("incorrect creation time => %v, want %v", service.CreationTime, tnow) 181 } 182 183 if len(service.Ports) != len(localSvc.Spec.Ports) { 184 t.Fatalf("incorrect number of ports => %v, want %v", 185 len(service.Ports), len(localSvc.Spec.Ports)) 186 } 187 188 if service.External() { 189 t.Fatal("service should not be external") 190 } 191 192 if service.Hostname != ServiceHostname(serviceName, namespace, domainSuffix) { 193 t.Fatalf("service hostname incorrect => %q, want %q", 194 service.Hostname, ServiceHostname(serviceName, namespace, domainSuffix)) 195 } 196 197 if service.Address != ip { 198 t.Fatalf("service IP incorrect => %q, want %q", service.Address, ip) 199 } 200 201 sa := service.ServiceAccounts 202 if sa == nil || len(sa) != 4 { 203 t.Fatalf("number of service accounts is incorrect") 204 } 205 expected := []string{ 206 saC, saD, 207 "spiffe://company.com/ns/default/sa/" + saA, 208 "spiffe://company.com/ns/default/sa/" + saB, 209 } 210 if !reflect.DeepEqual(sa, expected) { 211 t.Fatalf("Unexpected service accounts %v (expecting %v)", sa, expected) 212 } 213} 214 215func TestServiceConversionWithEmptyServiceAccountsAnnotation(t *testing.T) { 216 serviceName := "service1" 217 namespace := "default" 218 219 ip := "10.0.0.1" 220 221 localSvc := coreV1.Service{ 222 ObjectMeta: metaV1.ObjectMeta{ 223 Name: serviceName, 224 Namespace: namespace, 225 Annotations: map[string]string{}, 226 }, 227 Spec: coreV1.ServiceSpec{ 228 ClusterIP: ip, 229 Ports: []coreV1.ServicePort{ 230 { 231 Name: "http", 232 Port: 8080, 233 Protocol: coreV1.ProtocolTCP, 234 }, 235 { 236 Name: "https", 237 Protocol: coreV1.ProtocolTCP, 238 Port: 443, 239 }, 240 }, 241 }, 242 } 243 244 service := ConvertService(localSvc, domainSuffix, clusterID) 245 if service == nil { 246 t.Fatalf("could not convert service") 247 } 248 249 sa := service.ServiceAccounts 250 if len(sa) != 0 { 251 t.Fatalf("number of service accounts is incorrect: %d, expected 0", len(sa)) 252 } 253} 254 255func TestExternalServiceConversion(t *testing.T) { 256 serviceName := "service1" 257 namespace := "default" 258 259 extSvc := coreV1.Service{ 260 ObjectMeta: metaV1.ObjectMeta{ 261 Name: serviceName, 262 Namespace: namespace, 263 }, 264 Spec: coreV1.ServiceSpec{ 265 Ports: []coreV1.ServicePort{ 266 { 267 Name: "http", 268 Port: 80, 269 Protocol: coreV1.ProtocolTCP, 270 }, 271 }, 272 Type: coreV1.ServiceTypeExternalName, 273 ExternalName: "google.com", 274 }, 275 } 276 277 service := ConvertService(extSvc, domainSuffix, clusterID) 278 if service == nil { 279 t.Fatalf("could not convert external service") 280 } 281 282 if len(service.Ports) != len(extSvc.Spec.Ports) { 283 t.Fatalf("incorrect number of ports => %v, want %v", 284 len(service.Ports), len(extSvc.Spec.Ports)) 285 } 286 287 if !service.External() { 288 t.Fatal("service should be external") 289 } 290 291 if service.Hostname != ServiceHostname(serviceName, namespace, domainSuffix) { 292 t.Fatalf("service hostname incorrect => %q, want %q", 293 service.Hostname, ServiceHostname(serviceName, namespace, domainSuffix)) 294 } 295} 296 297func TestExternalClusterLocalServiceConversion(t *testing.T) { 298 serviceName := "service1" 299 namespace := "default" 300 301 extSvc := coreV1.Service{ 302 ObjectMeta: metaV1.ObjectMeta{ 303 Name: serviceName, 304 Namespace: namespace, 305 }, 306 Spec: coreV1.ServiceSpec{ 307 Ports: []coreV1.ServicePort{ 308 { 309 Name: "http", 310 Port: 80, 311 Protocol: coreV1.ProtocolTCP, 312 }, 313 }, 314 Type: coreV1.ServiceTypeExternalName, 315 ExternalName: "some.test.svc.cluster.local", 316 }, 317 } 318 319 domainSuffix := "cluster.local" 320 321 service := ConvertService(extSvc, domainSuffix, clusterID) 322 if service == nil { 323 t.Fatalf("could not convert external service") 324 } 325 326 if len(service.Ports) != len(extSvc.Spec.Ports) { 327 t.Fatalf("incorrect number of ports => %v, want %v", 328 len(service.Ports), len(extSvc.Spec.Ports)) 329 } 330 331 if !service.External() { 332 t.Fatal("ExternalName service (even if .cluster.local) should be external") 333 } 334 335 if service.Hostname != ServiceHostname(serviceName, namespace, domainSuffix) { 336 t.Fatalf("service hostname incorrect => %q, want %q", 337 service.Hostname, ServiceHostname(serviceName, namespace, domainSuffix)) 338 } 339} 340 341func TestLBServiceConversion(t *testing.T) { 342 serviceName := "service1" 343 namespace := "default" 344 345 addresses := []coreV1.LoadBalancerIngress{ 346 { 347 IP: "127.68.32.112", 348 }, 349 { 350 IP: "127.68.32.113", 351 }, 352 { 353 Hostname: "127.68.32.114", 354 }, 355 { 356 Hostname: "127.68.32.115", 357 }, 358 } 359 360 extSvc := coreV1.Service{ 361 ObjectMeta: metaV1.ObjectMeta{ 362 Name: serviceName, 363 Namespace: namespace, 364 }, 365 Spec: coreV1.ServiceSpec{ 366 Ports: []coreV1.ServicePort{ 367 { 368 Name: "http", 369 Port: 80, 370 Protocol: coreV1.ProtocolTCP, 371 }, 372 }, 373 Type: coreV1.ServiceTypeLoadBalancer, 374 }, 375 Status: coreV1.ServiceStatus{ 376 LoadBalancer: coreV1.LoadBalancerStatus{ 377 Ingress: addresses, 378 }, 379 }, 380 } 381 382 service := ConvertService(extSvc, domainSuffix, clusterID) 383 if service == nil { 384 t.Fatalf("could not convert external service") 385 } 386 387 if len(service.Attributes.ClusterExternalAddresses[clusterID]) == 0 { 388 t.Fatalf("no load balancer addresses found") 389 } 390 391 for i, addr := range addresses { 392 var want string 393 if len(addr.IP) > 0 { 394 want = addr.IP 395 } else { 396 want = addr.Hostname 397 } 398 got := service.Attributes.ClusterExternalAddresses[clusterID][i] 399 if got != want { 400 t.Fatalf("Expected address %s but got %s", want, got) 401 } 402 } 403} 404 405func TestProbesToPortsConversion(t *testing.T) { 406 407 expected := model.PortList{ 408 { 409 Name: "mgmt-3306", 410 Port: 3306, 411 Protocol: protocol.TCP, 412 }, 413 { 414 Name: "mgmt-9080", 415 Port: 9080, 416 Protocol: protocol.HTTP, 417 }, 418 } 419 420 handlers := []coreV1.Handler{ 421 { 422 TCPSocket: &coreV1.TCPSocketAction{ 423 Port: intstr.IntOrString{StrVal: "mysql", Type: intstr.String}, 424 }, 425 }, 426 { 427 TCPSocket: &coreV1.TCPSocketAction{ 428 Port: intstr.IntOrString{IntVal: 3306, Type: intstr.Int}, 429 }, 430 }, 431 { 432 HTTPGet: &coreV1.HTTPGetAction{ 433 Path: "/foo", 434 Port: intstr.IntOrString{StrVal: "http-two", Type: intstr.String}, 435 }, 436 }, 437 { 438 HTTPGet: &coreV1.HTTPGetAction{ 439 Path: "/foo", 440 Port: intstr.IntOrString{IntVal: 9080, Type: intstr.Int}, 441 }, 442 }, 443 } 444 445 podSpec := &coreV1.PodSpec{ 446 Containers: []coreV1.Container{ 447 { 448 Name: "scooby", 449 Ports: []coreV1.ContainerPort{ 450 { 451 Name: "mysql", 452 ContainerPort: 3306, 453 }, 454 { 455 Name: "http-two", 456 ContainerPort: 9080, 457 }, 458 { 459 Name: "http", 460 ContainerPort: 80, 461 }, 462 }, 463 LivenessProbe: &coreV1.Probe{}, 464 ReadinessProbe: &coreV1.Probe{}, 465 }, 466 }, 467 } 468 469 for _, handler1 := range handlers { 470 for _, handler2 := range handlers { 471 if (handler1.TCPSocket != nil && handler2.TCPSocket != nil) || 472 (handler1.HTTPGet != nil && handler2.HTTPGet != nil) { 473 continue 474 } 475 476 podSpec.Containers[0].LivenessProbe.Handler = handler1 477 podSpec.Containers[0].ReadinessProbe.Handler = handler2 478 479 mgmtPorts, err := ConvertProbesToPorts(podSpec) 480 if err != nil { 481 t.Fatalf("Failed to convert Probes to Ports: %v", err) 482 } 483 484 if !reflect.DeepEqual(mgmtPorts, expected) { 485 t.Fatalf("incorrect number of management ports => %v, want %v", 486 len(mgmtPorts), len(expected)) 487 } 488 } 489 } 490} 491 492func TestSecureNamingSANCustomIdentity(t *testing.T) { 493 494 pod := &coreV1.Pod{} 495 496 identity := "foo" 497 498 pod.Annotations = make(map[string]string) 499 pod.Annotations[annotation.AlphaIdentity.Name] = identity 500 501 san := SecureNamingSAN(pod) 502 503 expectedSAN := fmt.Sprintf("spiffe://%v/%v", spiffe.GetTrustDomain(), identity) 504 505 if san != expectedSAN { 506 t.Fatalf("SAN match failed, SAN:%v expectedSAN:%v", san, expectedSAN) 507 } 508 509} 510 511func TestSecureNamingSAN(t *testing.T) { 512 513 pod := &coreV1.Pod{} 514 515 pod.Annotations = make(map[string]string) 516 517 ns := "anything" 518 sa := "foo" 519 pod.Namespace = ns 520 pod.Spec.ServiceAccountName = sa 521 522 san := SecureNamingSAN(pod) 523 524 expectedSAN := fmt.Sprintf("spiffe://%v/ns/%v/sa/%v", spiffe.GetTrustDomain(), ns, sa) 525 526 if san != expectedSAN { 527 t.Fatalf("SAN match failed, SAN:%v expectedSAN:%v", san, expectedSAN) 528 } 529} 530