k8s.io/kubernetes@v1.29.3/test/e2e/framework/providers/gce/firewall.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package gce
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	"strconv"
    24  	"strings"
    25  	"time"
    26  
    27  	compute "google.golang.org/api/compute/v1"
    28  
    29  	v1 "k8s.io/api/core/v1"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"k8s.io/apimachinery/pkg/util/wait"
    32  	cloudprovider "k8s.io/cloud-provider"
    33  	"k8s.io/kubernetes/test/e2e/framework"
    34  	gcecloud "k8s.io/legacy-cloud-providers/gce"
    35  )
    36  
    37  // MakeFirewallNameForLBService return the expected firewall name for a LB service.
    38  // This should match the formatting of makeFirewallName() in pkg/cloudprovider/providers/gce/gce_loadbalancer.go
    39  func MakeFirewallNameForLBService(name string) string {
    40  	return fmt.Sprintf("k8s-fw-%s", name)
    41  }
    42  
    43  // ConstructFirewallForLBService returns the expected GCE firewall rule for a loadbalancer type service
    44  func ConstructFirewallForLBService(svc *v1.Service, nodeTag string) *compute.Firewall {
    45  	if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
    46  		framework.Failf("can not construct firewall rule for non-loadbalancer type service")
    47  	}
    48  	fw := compute.Firewall{}
    49  	fw.Name = MakeFirewallNameForLBService(cloudprovider.DefaultLoadBalancerName(svc))
    50  	fw.TargetTags = []string{nodeTag}
    51  	if svc.Spec.LoadBalancerSourceRanges == nil {
    52  		fw.SourceRanges = []string{"0.0.0.0/0"}
    53  	} else {
    54  		fw.SourceRanges = svc.Spec.LoadBalancerSourceRanges
    55  	}
    56  	for _, sp := range svc.Spec.Ports {
    57  		fw.Allowed = append(fw.Allowed, &compute.FirewallAllowed{
    58  			IPProtocol: strings.ToLower(string(sp.Protocol)),
    59  			Ports:      []string{strconv.Itoa(int(sp.Port))},
    60  		})
    61  	}
    62  	return &fw
    63  }
    64  
    65  // MakeHealthCheckFirewallNameForLBService returns the firewall name used by the GCE load
    66  // balancers for performing health checks.
    67  func MakeHealthCheckFirewallNameForLBService(clusterID, name string, isNodesHealthCheck bool) string {
    68  	return gcecloud.MakeHealthCheckFirewallName(clusterID, name, isNodesHealthCheck)
    69  }
    70  
    71  // ConstructHealthCheckFirewallForLBService returns the expected GCE firewall rule for a loadbalancer type service
    72  func ConstructHealthCheckFirewallForLBService(clusterID string, svc *v1.Service, nodeTag string, isNodesHealthCheck bool) *compute.Firewall {
    73  	if svc.Spec.Type != v1.ServiceTypeLoadBalancer {
    74  		framework.Failf("can not construct firewall rule for non-loadbalancer type service")
    75  	}
    76  	fw := compute.Firewall{}
    77  	fw.Name = MakeHealthCheckFirewallNameForLBService(clusterID, cloudprovider.DefaultLoadBalancerName(svc), isNodesHealthCheck)
    78  	fw.TargetTags = []string{nodeTag}
    79  	fw.SourceRanges = gcecloud.L4LoadBalancerSrcRanges()
    80  	healthCheckPort := gcecloud.GetNodesHealthCheckPort()
    81  	if !isNodesHealthCheck {
    82  		healthCheckPort = svc.Spec.HealthCheckNodePort
    83  	}
    84  	fw.Allowed = []*compute.FirewallAllowed{
    85  		{
    86  			IPProtocol: "tcp",
    87  			Ports:      []string{fmt.Sprintf("%d", healthCheckPort)},
    88  		},
    89  	}
    90  	return &fw
    91  }
    92  
    93  // PackProtocolsPortsFromFirewall packs protocols and ports in an unified way for verification.
    94  func PackProtocolsPortsFromFirewall(alloweds []*compute.FirewallAllowed) []string {
    95  	protocolPorts := []string{}
    96  	for _, allowed := range alloweds {
    97  		for _, port := range allowed.Ports {
    98  			protocolPorts = append(protocolPorts, strings.ToLower(allowed.IPProtocol+"/"+port))
    99  		}
   100  	}
   101  	return protocolPorts
   102  }
   103  
   104  type portRange struct {
   105  	protocol string
   106  	min, max int
   107  }
   108  
   109  func toPortRange(s string) (pr portRange, err error) {
   110  	protoPorts := strings.Split(s, "/")
   111  	// Set protocol
   112  	pr.protocol = strings.ToUpper(protoPorts[0])
   113  
   114  	if len(protoPorts) != 2 {
   115  		return pr, fmt.Errorf("expected a single '/' in %q", s)
   116  	}
   117  
   118  	ports := strings.Split(protoPorts[1], "-")
   119  	switch len(ports) {
   120  	case 1:
   121  		v, err := strconv.Atoi(ports[0])
   122  		if err != nil {
   123  			return pr, err
   124  		}
   125  		pr.min, pr.max = v, v
   126  	case 2:
   127  		start, err := strconv.Atoi(ports[0])
   128  		if err != nil {
   129  			return pr, err
   130  		}
   131  		end, err := strconv.Atoi(ports[1])
   132  		if err != nil {
   133  			return pr, err
   134  		}
   135  		pr.min, pr.max = start, end
   136  	default:
   137  		return pr, fmt.Errorf("unexpected range value %q", protoPorts[1])
   138  	}
   139  
   140  	return pr, nil
   141  }
   142  
   143  // isPortsSubset asserts that the "requiredPorts" are covered by the "coverage" ports.
   144  // requiredPorts - must be single-port, examples: 'tcp/50', 'udp/80'.
   145  // coverage - single or port-range values, example: 'tcp/50', 'udp/80-1000'.
   146  // Returns true if every requiredPort exists in the list of coverage rules.
   147  func isPortsSubset(requiredPorts, coverage []string) error {
   148  	for _, reqPort := range requiredPorts {
   149  		rRange, err := toPortRange(reqPort)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		if rRange.min != rRange.max {
   154  			return fmt.Errorf("requiring a range is not supported: %q", reqPort)
   155  		}
   156  
   157  		var covered bool
   158  		for _, c := range coverage {
   159  			cRange, err := toPortRange(c)
   160  			if err != nil {
   161  				return err
   162  			}
   163  
   164  			if rRange.protocol != cRange.protocol {
   165  				continue
   166  			}
   167  
   168  			if rRange.min >= cRange.min && rRange.min <= cRange.max {
   169  				covered = true
   170  				break
   171  			}
   172  		}
   173  
   174  		if !covered {
   175  			return fmt.Errorf("%q is not covered by %v", reqPort, coverage)
   176  		}
   177  	}
   178  	return nil
   179  }
   180  
   181  // SameStringArray verifies whether two string arrays have the same strings, return error if not.
   182  // Order does not matter.
   183  // When `include` is set to true, verifies whether result includes all elements from expected.
   184  func SameStringArray(result, expected []string, include bool) error {
   185  	res := sets.NewString(result...)
   186  	exp := sets.NewString(expected...)
   187  	if !include {
   188  		diff := res.Difference(exp)
   189  		if len(diff) != 0 {
   190  			return fmt.Errorf("found differences: %v", diff)
   191  		}
   192  	} else {
   193  		if !res.IsSuperset(exp) {
   194  			return fmt.Errorf("some elements are missing: expected %v, got %v", expected, result)
   195  		}
   196  	}
   197  	return nil
   198  }
   199  
   200  // VerifyFirewallRule verifies whether the result firewall is consistent with the expected firewall.
   201  // When `portsSubset` is false, match given ports exactly. Otherwise, only check ports are included.
   202  func VerifyFirewallRule(res, exp *compute.Firewall, network string, portsSubset bool) error {
   203  	if res == nil || exp == nil {
   204  		return fmt.Errorf("res and exp must not be nil")
   205  	}
   206  	if res.Name != exp.Name {
   207  		return fmt.Errorf("incorrect name: %v, expected %v", res.Name, exp.Name)
   208  	}
   209  	// Sample Network value: https://www.googleapis.com/compute/v1/projects/{project-id}/global/networks/e2e
   210  	if !strings.HasSuffix(res.Network, "/"+network) {
   211  		return fmt.Errorf("incorrect network: %v, expected ends with: %v", res.Network, "/"+network)
   212  	}
   213  
   214  	actualPorts := PackProtocolsPortsFromFirewall(res.Allowed)
   215  	expPorts := PackProtocolsPortsFromFirewall(exp.Allowed)
   216  	if portsSubset {
   217  		if err := isPortsSubset(expPorts, actualPorts); err != nil {
   218  			return fmt.Errorf("incorrect allowed protocol ports: %w", err)
   219  		}
   220  	} else {
   221  		if err := SameStringArray(actualPorts, expPorts, false); err != nil {
   222  			return fmt.Errorf("incorrect allowed protocols ports: %w", err)
   223  		}
   224  	}
   225  
   226  	if err := SameStringArray(res.SourceRanges, exp.SourceRanges, false); err != nil {
   227  		return fmt.Errorf("incorrect source ranges %v, expected %v: %w", res.SourceRanges, exp.SourceRanges, err)
   228  	}
   229  	if err := SameStringArray(res.SourceTags, exp.SourceTags, false); err != nil {
   230  		return fmt.Errorf("incorrect source tags %v, expected %v: %w", res.SourceTags, exp.SourceTags, err)
   231  	}
   232  	if err := SameStringArray(res.TargetTags, exp.TargetTags, false); err != nil {
   233  		return fmt.Errorf("incorrect target tags %v, expected %v: %w", res.TargetTags, exp.TargetTags, err)
   234  	}
   235  	return nil
   236  }
   237  
   238  // WaitForFirewallRule waits for the specified firewall existence
   239  func WaitForFirewallRule(ctx context.Context, gceCloud *gcecloud.Cloud, fwName string, exist bool, timeout time.Duration) (*compute.Firewall, error) {
   240  	framework.Logf("Waiting up to %v for firewall %v exist=%v", timeout, fwName, exist)
   241  	var fw *compute.Firewall
   242  	var err error
   243  
   244  	condition := func(ctx context.Context) (bool, error) {
   245  		fw, err = gceCloud.GetFirewall(fwName)
   246  		if err != nil && exist ||
   247  			err == nil && !exist ||
   248  			err != nil && !exist && !IsGoogleAPIHTTPErrorCode(err, http.StatusNotFound) {
   249  			return false, nil
   250  		}
   251  		return true, nil
   252  	}
   253  
   254  	if err := wait.PollUntilContextTimeout(ctx, 5*time.Second, timeout, true, condition); err != nil {
   255  		return nil, fmt.Errorf("error waiting for firewall %v exist=%v", fwName, exist)
   256  	}
   257  	return fw, nil
   258  }