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 }