1
2
3
4
19
20 package gce
21
22 import (
23 "context"
24 "crypto/sha256"
25 "encoding/json"
26 "fmt"
27 "net/http"
28 "os/exec"
29 "strconv"
30 "strings"
31 "time"
32
33 "github.com/onsi/ginkgo/v2"
34 compute "google.golang.org/api/compute/v1"
35 "google.golang.org/api/googleapi"
36 v1 "k8s.io/api/core/v1"
37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38 "k8s.io/apimachinery/pkg/util/wait"
39 clientset "k8s.io/client-go/kubernetes"
40 "k8s.io/kubernetes/test/e2e/framework"
41 e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
42 utilexec "k8s.io/utils/exec"
43 )
44
45 const (
46
47 uidConfigMap = "ingress-uid"
48 uidKey = "uid"
49
50
51
52 k8sPrefix = "k8s-"
53
54
55
56 clusterDelimiter = "--"
57
58
59
60
61 maxAge = 48 * time.Hour
62
63
64
65 nameLenLimit = 62
66
67 negBackend = backendType("networkEndpointGroup")
68 igBackend = backendType("instanceGroup")
69 )
70
71 type backendType string
72
73
74 type IngressController struct {
75 Ns string
76 UID string
77 staticIPName string
78 Client clientset.Interface
79 Cloud framework.CloudConfig
80 }
81
82
83 func (cont *IngressController) CleanupIngressController(ctx context.Context) error {
84 return cont.CleanupIngressControllerWithTimeout(ctx, e2eservice.LoadBalancerCleanupTimeout)
85 }
86
87
88
89 func (cont *IngressController) CleanupIngressControllerWithTimeout(ctx context.Context, timeout time.Duration) error {
90 pollErr := wait.PollWithContext(ctx, 5*time.Second, timeout, func(ctx context.Context) (bool, error) {
91 if err := cont.Cleanup(false); err != nil {
92 framework.Logf("Monitoring glbc's cleanup of gce resources:\n%v", err)
93 return false, nil
94 }
95 return true, nil
96 })
97
98
99
100 ginkgo.By("Performing final delete of any remaining resources")
101 if cleanupErr := cont.Cleanup(true); cleanupErr != nil {
102 ginkgo.By(fmt.Sprintf("WARNING: possibly leaked resources: %v\n", cleanupErr))
103 } else {
104 ginkgo.By("No resources leaked.")
105 }
106
107
108
109
110
111 if ipErr := wait.PollWithContext(ctx, 5*time.Second, 1*time.Minute, func(ctx context.Context) (bool, error) {
112 if err := cont.deleteStaticIPs(); err != nil {
113 framework.Logf("Failed to delete static-ip: %v\n", err)
114 return false, nil
115 }
116 return true, nil
117 }); ipErr != nil {
118
119
120 ginkgo.By(fmt.Sprintf("WARNING: possibly leaked static IP: %v\n", ipErr))
121 }
122
123
124
125 if pollErr != nil {
126 return fmt.Errorf("error: L7 controller failed to delete all cloud resources on time. %v", pollErr)
127 }
128 return nil
129 }
130
131 func (cont *IngressController) getL7AddonUID(ctx context.Context) (string, error) {
132 framework.Logf("Retrieving UID from config map: %v/%v", metav1.NamespaceSystem, uidConfigMap)
133 cm, err := cont.Client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(ctx, uidConfigMap, metav1.GetOptions{})
134 if err != nil {
135 return "", err
136 }
137 if uid, ok := cm.Data[uidKey]; ok {
138 return uid, nil
139 }
140 return "", fmt.Errorf("Could not find cluster UID for L7 addon pod")
141 }
142
143 func (cont *IngressController) deleteForwardingRule(del bool) string {
144 msg := ""
145 fwList := []compute.ForwardingRule{}
146 for _, regex := range []string{fmt.Sprintf("%vfw-.*%v.*", k8sPrefix, clusterDelimiter), fmt.Sprintf("%vfws-.*%v.*", k8sPrefix, clusterDelimiter)} {
147 gcloudComputeResourceList("forwarding-rules", regex, cont.Cloud.ProjectID, &fwList)
148 if len(fwList) == 0 {
149 continue
150 }
151 for _, f := range fwList {
152 if !cont.canDelete(f.Name, f.CreationTimestamp, del) {
153 continue
154 }
155 if del {
156 GcloudComputeResourceDelete("forwarding-rules", f.Name, cont.Cloud.ProjectID, "--global")
157 } else {
158 msg += fmt.Sprintf("%v (forwarding rule)\n", f.Name)
159 }
160 }
161 }
162 return msg
163 }
164
165 func (cont *IngressController) deleteAddresses(del bool) string {
166 msg := ""
167 ipList := []compute.Address{}
168 regex := fmt.Sprintf("%vfw-.*%v.*", k8sPrefix, clusterDelimiter)
169 gcloudComputeResourceList("addresses", regex, cont.Cloud.ProjectID, &ipList)
170 if len(ipList) != 0 {
171 for _, ip := range ipList {
172 if !cont.canDelete(ip.Name, ip.CreationTimestamp, del) {
173 continue
174 }
175 if del {
176 GcloudComputeResourceDelete("addresses", ip.Name, cont.Cloud.ProjectID, "--global")
177 } else {
178 msg += fmt.Sprintf("%v (static-ip)\n", ip.Name)
179 }
180 }
181 }
182 return msg
183 }
184
185 func (cont *IngressController) deleteTargetProxy(del bool) string {
186 msg := ""
187 tpList := []compute.TargetHttpProxy{}
188 regex := fmt.Sprintf("%vtp-.*%v.*", k8sPrefix, clusterDelimiter)
189 gcloudComputeResourceList("target-http-proxies", regex, cont.Cloud.ProjectID, &tpList)
190 if len(tpList) != 0 {
191 for _, t := range tpList {
192 if !cont.canDelete(t.Name, t.CreationTimestamp, del) {
193 continue
194 }
195 if del {
196 GcloudComputeResourceDelete("target-http-proxies", t.Name, cont.Cloud.ProjectID)
197 } else {
198 msg += fmt.Sprintf("%v (target-http-proxy)\n", t.Name)
199 }
200 }
201 }
202 tpsList := []compute.TargetHttpsProxy{}
203 regex = fmt.Sprintf("%vtps-.*%v.*", k8sPrefix, clusterDelimiter)
204 gcloudComputeResourceList("target-https-proxies", regex, cont.Cloud.ProjectID, &tpsList)
205 if len(tpsList) != 0 {
206 for _, t := range tpsList {
207 if !cont.canDelete(t.Name, t.CreationTimestamp, del) {
208 continue
209 }
210 if del {
211 GcloudComputeResourceDelete("target-https-proxies", t.Name, cont.Cloud.ProjectID)
212 } else {
213 msg += fmt.Sprintf("%v (target-https-proxy)\n", t.Name)
214 }
215 }
216 }
217 return msg
218 }
219
220 func (cont *IngressController) deleteURLMap(del bool) (msg string) {
221 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
222 umList, err := gceCloud.ListURLMaps()
223 if err != nil {
224 if cont.isHTTPErrorCode(err, http.StatusNotFound) {
225 return msg
226 }
227 return fmt.Sprintf("Failed to list url maps: %v", err)
228 }
229 if len(umList) == 0 {
230 return msg
231 }
232 for _, um := range umList {
233 if !cont.canDelete(um.Name, um.CreationTimestamp, del) {
234 continue
235 }
236 if del {
237 framework.Logf("Deleting url-map: %s", um.Name)
238 if err := gceCloud.DeleteURLMap(um.Name); err != nil &&
239 !cont.isHTTPErrorCode(err, http.StatusNotFound) {
240 msg += fmt.Sprintf("Failed to delete url map %v\n", um.Name)
241 }
242 } else {
243 msg += fmt.Sprintf("%v (url-map)\n", um.Name)
244 }
245 }
246 return msg
247 }
248
249 func (cont *IngressController) deleteBackendService(del bool) (msg string) {
250 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
251 beList, err := gceCloud.ListGlobalBackendServices()
252 if err != nil {
253 if cont.isHTTPErrorCode(err, http.StatusNotFound) {
254 return msg
255 }
256 return fmt.Sprintf("Failed to list backend services: %v", err)
257 }
258 if len(beList) == 0 {
259 framework.Logf("No backend services found")
260 return msg
261 }
262 for _, be := range beList {
263 if !cont.canDelete(be.Name, be.CreationTimestamp, del) {
264 continue
265 }
266 if del {
267 framework.Logf("Deleting backed-service: %s", be.Name)
268 if err := gceCloud.DeleteGlobalBackendService(be.Name); err != nil &&
269 !cont.isHTTPErrorCode(err, http.StatusNotFound) {
270 msg += fmt.Sprintf("Failed to delete backend service %v: %v\n", be.Name, err)
271 }
272 } else {
273 msg += fmt.Sprintf("%v (backend-service)\n", be.Name)
274 }
275 }
276 return msg
277 }
278
279 func (cont *IngressController) deleteHTTPHealthCheck(del bool) (msg string) {
280 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
281 hcList, err := gceCloud.ListHTTPHealthChecks()
282 if err != nil {
283 if cont.isHTTPErrorCode(err, http.StatusNotFound) {
284 return msg
285 }
286 return fmt.Sprintf("Failed to list HTTP health checks: %v", err)
287 }
288 if len(hcList) == 0 {
289 return msg
290 }
291 for _, hc := range hcList {
292 if !cont.canDelete(hc.Name, hc.CreationTimestamp, del) {
293 continue
294 }
295 if del {
296 framework.Logf("Deleting http-health-check: %s", hc.Name)
297 if err := gceCloud.DeleteHTTPHealthCheck(hc.Name); err != nil &&
298 !cont.isHTTPErrorCode(err, http.StatusNotFound) {
299 msg += fmt.Sprintf("Failed to delete HTTP health check %v\n", hc.Name)
300 }
301 } else {
302 msg += fmt.Sprintf("%v (http-health-check)\n", hc.Name)
303 }
304 }
305 return msg
306 }
307
308 func (cont *IngressController) deleteSSLCertificate(del bool) (msg string) {
309 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
310 sslList, err := gceCloud.ListSslCertificates()
311 if err != nil {
312 if cont.isHTTPErrorCode(err, http.StatusNotFound) {
313 return msg
314 }
315 return fmt.Sprintf("Failed to list ssl certificates: %v", err)
316 }
317 if len(sslList) != 0 {
318 for _, s := range sslList {
319 if !cont.canDelete(s.Name, s.CreationTimestamp, del) {
320 continue
321 }
322 if del {
323 framework.Logf("Deleting ssl-certificate: %s", s.Name)
324 if err := gceCloud.DeleteSslCertificate(s.Name); err != nil &&
325 !cont.isHTTPErrorCode(err, http.StatusNotFound) {
326 msg += fmt.Sprintf("Failed to delete ssl certificates: %v\n", s.Name)
327 }
328 } else {
329 msg += fmt.Sprintf("%v (ssl-certificate)\n", s.Name)
330 }
331 }
332 }
333 return msg
334 }
335
336 func (cont *IngressController) deleteInstanceGroup(del bool) (msg string) {
337 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
338
339
340 igList, err := gceCloud.ListInstanceGroups(cont.Cloud.Zone)
341 if err != nil {
342 if cont.isHTTPErrorCode(err, http.StatusNotFound) {
343 return msg
344 }
345 return fmt.Sprintf("Failed to list instance groups: %v", err)
346 }
347 if len(igList) == 0 {
348 return msg
349 }
350 for _, ig := range igList {
351 if !cont.canDelete(ig.Name, ig.CreationTimestamp, del) {
352 continue
353 }
354 if del {
355 framework.Logf("Deleting instance-group: %s", ig.Name)
356 if err := gceCloud.DeleteInstanceGroup(ig.Name, cont.Cloud.Zone); err != nil &&
357 !cont.isHTTPErrorCode(err, http.StatusNotFound) {
358 msg += fmt.Sprintf("Failed to delete instance group %v\n", ig.Name)
359 }
360 } else {
361 msg += fmt.Sprintf("%v (instance-group)\n", ig.Name)
362 }
363 }
364 return msg
365 }
366
367 func (cont *IngressController) deleteNetworkEndpointGroup(del bool) (msg string) {
368 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
369
370
371 negList, err := gceCloud.ListNetworkEndpointGroup(cont.Cloud.Zone)
372 if err != nil {
373 if cont.isHTTPErrorCode(err, http.StatusNotFound) {
374 return msg
375 }
376
377 framework.Logf("Failed to list network endpoint group: %v", err)
378 return msg
379 }
380 if len(negList) == 0 {
381 return msg
382 }
383 for _, neg := range negList {
384 if !cont.canDeleteNEG(neg.Name, neg.CreationTimestamp, del) {
385 continue
386 }
387 if del {
388 framework.Logf("Deleting network-endpoint-group: %s", neg.Name)
389 if err := gceCloud.DeleteNetworkEndpointGroup(neg.Name, cont.Cloud.Zone); err != nil &&
390 !cont.isHTTPErrorCode(err, http.StatusNotFound) {
391 msg += fmt.Sprintf("Failed to delete network endpoint group %v\n", neg.Name)
392 }
393 } else {
394 msg += fmt.Sprintf("%v (network-endpoint-group)\n", neg.Name)
395 }
396 }
397 return msg
398 }
399
400
401
402
403
404 func (cont *IngressController) canDelete(resourceName, creationTimestamp string, delOldResources bool) bool {
405
406 splitName := strings.Split(resourceName, clusterDelimiter)
407 if !strings.HasPrefix(resourceName, k8sPrefix) || len(splitName) != 2 {
408 return false
409 }
410
411
412
413 truncatedClusterUID := splitName[1]
414 if len(truncatedClusterUID) >= 1 && strings.HasSuffix(truncatedClusterUID, "0") {
415 truncatedClusterUID = truncatedClusterUID[:len(truncatedClusterUID)-1]
416 }
417
418
419
420 if strings.HasPrefix(cont.UID, truncatedClusterUID) {
421 return true
422 }
423 if !delOldResources {
424 return false
425 }
426 return canDeleteWithTimestamp(resourceName, creationTimestamp)
427 }
428
429
430
431 func (cont *IngressController) canDeleteNEG(resourceName, creationTimestamp string, delOldResources bool) bool {
432 if !strings.HasPrefix(resourceName, "k8s") {
433 return false
434 }
435
436 if strings.Contains(resourceName, cont.UID) {
437 return true
438 }
439
440 if !delOldResources {
441 return false
442 }
443
444 return canDeleteWithTimestamp(resourceName, creationTimestamp)
445 }
446
447 func canDeleteWithTimestamp(resourceName, creationTimestamp string) bool {
448 createdTime, err := time.Parse(time.RFC3339, creationTimestamp)
449 if err != nil {
450 framework.Logf("WARNING: Failed to parse creation timestamp %v for %v: %v", creationTimestamp, resourceName, err)
451 return false
452 }
453 if time.Since(createdTime) > maxAge {
454 framework.Logf("%v created on %v IS too old", resourceName, creationTimestamp)
455 return true
456 }
457 return false
458 }
459
460 func (cont *IngressController) deleteFirewallRule(del bool) (msg string) {
461 fwList := []compute.Firewall{}
462 regex := fmt.Sprintf("%vfw-l7%v.*", k8sPrefix, clusterDelimiter)
463 gcloudComputeResourceList("firewall-rules", regex, cont.Cloud.ProjectID, &fwList)
464 if len(fwList) != 0 {
465 for _, f := range fwList {
466 if !cont.canDelete(f.Name, f.CreationTimestamp, del) {
467 continue
468 }
469 if del {
470 GcloudComputeResourceDelete("firewall-rules", f.Name, cont.Cloud.ProjectID)
471 } else {
472 msg += fmt.Sprintf("%v (firewall rule)\n", f.Name)
473 }
474 }
475 }
476 return msg
477 }
478
479 func (cont *IngressController) isHTTPErrorCode(err error, code int) bool {
480 apiErr, ok := err.(*googleapi.Error)
481 return ok && apiErr.Code == code
482 }
483
484
485 func (cont *IngressController) WaitForNegBackendService(ctx context.Context, svcPorts map[string]v1.ServicePort) error {
486 return wait.PollWithContext(ctx, 5*time.Second, 1*time.Minute, func(ctx context.Context) (bool, error) {
487 err := cont.verifyBackendMode(svcPorts, negBackend)
488 if err != nil {
489 framework.Logf("Err while checking if backend service is using NEG: %v", err)
490 return false, nil
491 }
492 return true, nil
493 })
494 }
495
496
497 func (cont *IngressController) BackendServiceUsingNEG(svcPorts map[string]v1.ServicePort) error {
498 return cont.verifyBackendMode(svcPorts, negBackend)
499 }
500
501
502 func (cont *IngressController) BackendServiceUsingIG(svcPorts map[string]v1.ServicePort) error {
503 return cont.verifyBackendMode(svcPorts, igBackend)
504 }
505
506 func (cont *IngressController) verifyBackendMode(svcPorts map[string]v1.ServicePort, backendType backendType) error {
507 gceCloud := cont.Cloud.Provider.(*Provider).gceCloud
508 beList, err := gceCloud.ListGlobalBackendServices()
509 if err != nil {
510 return fmt.Errorf("failed to list backend services: %w", err)
511 }
512
513 hcList, err := gceCloud.ListHealthChecks()
514 if err != nil {
515 return fmt.Errorf("failed to list health checks: %w", err)
516 }
517
518
519 uid := cont.UID
520 if len(uid) > 8 {
521 uid = uid[:8]
522 }
523
524 matchingBackendService := 0
525 for svcName, sp := range svcPorts {
526 match := false
527 bsMatch := &compute.BackendService{}
528
529
530
531 negString := strings.Join([]string{uid, cont.Ns, svcName, fmt.Sprintf("%v", sp.Port)}, ";")
532 negHash := fmt.Sprintf("%x", sha256.Sum256([]byte(negString)))[:8]
533 for _, bs := range beList {
534
535 if backendType == igBackend && strings.Contains(bs.Name, strconv.Itoa(int(sp.NodePort))) {
536 match = true
537 bsMatch = bs
538 matchingBackendService++
539 break
540 }
541
542
543 if backendType == negBackend && strings.Contains(bs.Name, negHash) {
544 match = true
545 bsMatch = bs
546 matchingBackendService++
547 break
548 }
549 }
550
551 if match {
552 for _, be := range bsMatch.Backends {
553 if !strings.Contains(be.Group, string(backendType)) {
554 return fmt.Errorf("expect to find backends with type %q, but got backend group: %v", backendType, be.Group)
555 }
556 }
557
558
559 hcMatch := false
560 for _, hc := range hcList {
561 if hc.Name == bsMatch.Name {
562 hcMatch = true
563 break
564 }
565 }
566
567 if !hcMatch {
568 return fmt.Errorf("missing healthcheck for backendservice: %v", bsMatch.Name)
569 }
570 }
571 }
572
573 if matchingBackendService != len(svcPorts) {
574 beNames := []string{}
575 for _, be := range beList {
576 beNames = append(beNames, be.Name)
577 }
578 return fmt.Errorf("expect %d backend service with backend type: %v, but got %d matching backend service. Expect backend services for service ports: %v, but got backend services: %v", len(svcPorts), backendType, matchingBackendService, svcPorts, beNames)
579 }
580
581 return nil
582 }
583
584
585
586
587 func (cont *IngressController) Cleanup(del bool) error {
588
589
590 errMsg := cont.deleteForwardingRule(del)
591
592 errMsg += cont.deleteAddresses(del)
593
594 errMsg += cont.deleteTargetProxy(del)
595 errMsg += cont.deleteURLMap(del)
596 errMsg += cont.deleteBackendService(del)
597 errMsg += cont.deleteHTTPHealthCheck(del)
598
599 errMsg += cont.deleteInstanceGroup(del)
600 errMsg += cont.deleteNetworkEndpointGroup(del)
601 errMsg += cont.deleteFirewallRule(del)
602 errMsg += cont.deleteSSLCertificate(del)
603
604
605
606
607 if errMsg == "" {
608 return nil
609 }
610 return fmt.Errorf(errMsg)
611 }
612
613
614 func (cont *IngressController) Init(ctx context.Context) error {
615 uid, err := cont.getL7AddonUID(ctx)
616 if err != nil {
617 return err
618 }
619 cont.UID = uid
620
621 testName := fmt.Sprintf("k8s-fw-foo-app-X-%v--%v", cont.Ns, cont.UID)
622 if len(testName) > nameLenLimit {
623 framework.Logf("WARNING: test name including cluster UID: %v is over the GCE limit of %v", testName, nameLenLimit)
624 } else {
625 framework.Logf("Detected cluster UID %v", cont.UID)
626 }
627 return nil
628 }
629
630
631
632 func (cont *IngressController) deleteStaticIPs() error {
633 if cont.staticIPName != "" {
634 if err := GcloudComputeResourceDelete("addresses", cont.staticIPName, cont.Cloud.ProjectID, "--global"); err == nil {
635 cont.staticIPName = ""
636 } else {
637 return err
638 }
639 } else {
640 e2eIPs := []compute.Address{}
641 gcloudComputeResourceList("addresses", "e2e-.*", cont.Cloud.ProjectID, &e2eIPs)
642 ips := []string{}
643 for _, ip := range e2eIPs {
644 ips = append(ips, ip.Name)
645 }
646 framework.Logf("None of the remaining %d static-ips were created by this e2e: %v", len(ips), strings.Join(ips, ", "))
647 }
648 return nil
649 }
650
651
652 func gcloudComputeResourceList(resource, regex, project string, out interface{}) {
653
654
655 command := []string{
656 "compute", resource, "list",
657 fmt.Sprintf("--filter='name ~ \"%q\"'", regex),
658 fmt.Sprintf("--project=%v", project),
659 "-q", "--format=json",
660 }
661 output, err := exec.Command("gcloud", command...).Output()
662 if err != nil {
663 errCode := -1
664 errMsg := ""
665 if exitErr, ok := err.(utilexec.ExitError); ok {
666 errCode = exitErr.ExitStatus()
667 errMsg = exitErr.Error()
668 if osExitErr, ok := err.(*exec.ExitError); ok {
669 errMsg = fmt.Sprintf("%v, stderr %v", errMsg, string(osExitErr.Stderr))
670 }
671 }
672 framework.Logf("Error running gcloud command 'gcloud %s': err: %v, output: %v, status: %d, msg: %v", strings.Join(command, " "), err, string(output), errCode, errMsg)
673 }
674 if err := json.Unmarshal([]byte(output), out); err != nil {
675 framework.Logf("Error unmarshalling gcloud output for %v: %v, output: %v", resource, err, string(output))
676 }
677 }
678
679
680 func GcloudComputeResourceDelete(resource, name, project string, args ...string) error {
681 framework.Logf("Deleting %v: %v", resource, name)
682 argList := append([]string{"compute", resource, "delete", name, fmt.Sprintf("--project=%v", project), "-q"}, args...)
683 output, err := exec.Command("gcloud", argList...).CombinedOutput()
684 if err != nil {
685 framework.Logf("Error deleting %v, output: %v\nerror: %+v", resource, string(output), err)
686 }
687 return err
688 }
689
View as plain text