diff --git a/pkg/cloudprovider/providers/gce/BUILD b/pkg/cloudprovider/providers/gce/BUILD index 76be5d8671a..62576d352a2 100644 --- a/pkg/cloudprovider/providers/gce/BUILD +++ b/pkg/cloudprovider/providers/gce/BUILD @@ -14,6 +14,7 @@ go_library( "doc.go", "gce.go", "gce_addresses.go", + "gce_addresses_fakes.go", "gce_annotations.go", "gce_backendservice.go", "gce_cert.go", @@ -25,6 +26,7 @@ go_library( "gce_healthchecks.go", "gce_instancegroup.go", "gce_instances.go", + "gce_interfaces.go", "gce_loadbalancer.go", "gce_loadbalancer_external.go", "gce_loadbalancer_internal.go", @@ -81,6 +83,7 @@ go_test( srcs = [ "gce_disks_test.go", "gce_healthchecks_test.go", + "gce_loadbalancer_external_test.go", "gce_test.go", ], library = ":go_default_library", diff --git a/pkg/cloudprovider/providers/gce/gce_addresses_fakes.go b/pkg/cloudprovider/providers/gce/gce_addresses_fakes.go new file mode 100644 index 00000000000..bafb3e9ab0a --- /dev/null +++ b/pkg/cloudprovider/providers/gce/gce_addresses_fakes.go @@ -0,0 +1,77 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gce + +import ( + "fmt" + "net/http" + + compute "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" +) + +type FakeCloudAddressService struct { + count int + // reservedAddrs tracks usage of IP addresses + // Key is the IP address as a string + reservedAddrs map[string]bool + // addrsByRegionAndName + // Outer key is for region string; inner key is for address name. + addrsByRegionAndName map[string]map[string]*compute.Address +} + +func NewFakeCloudAddressService() *FakeCloudAddressService { + return &FakeCloudAddressService{ + reservedAddrs: make(map[string]bool), + addrsByRegionAndName: make(map[string]map[string]*compute.Address), + } +} + +func (cas *FakeCloudAddressService) ReserveRegionAddress(addr *compute.Address, region string) error { + if addr.Address == "" { + addr.Address = fmt.Sprintf("1.2.3.%d", cas.count) + cas.count++ + } + + if cas.reservedAddrs[addr.Address] { + return &googleapi.Error{Code: http.StatusConflict} + } + + if _, exists := cas.addrsByRegionAndName[region]; !exists { + cas.addrsByRegionAndName[region] = make(map[string]*compute.Address) + } + + if _, exists := cas.addrsByRegionAndName[region][addr.Name]; exists { + return &googleapi.Error{Code: http.StatusConflict} + } + + cas.addrsByRegionAndName[region][addr.Name] = addr + cas.reservedAddrs[addr.Address] = true + return nil +} + +func (cas *FakeCloudAddressService) GetRegionAddress(name, region string) (*compute.Address, error) { + if _, exists := cas.addrsByRegionAndName[region]; !exists { + return nil, &googleapi.Error{Code: http.StatusNotFound} + } + + if addr, exists := cas.addrsByRegionAndName[region][name]; !exists { + return nil, &googleapi.Error{Code: http.StatusNotFound} + } else { + return addr, nil + } +} diff --git a/pkg/cloudprovider/providers/gce/gce_interfaces.go b/pkg/cloudprovider/providers/gce/gce_interfaces.go new file mode 100644 index 00000000000..b8c98d082e1 --- /dev/null +++ b/pkg/cloudprovider/providers/gce/gce_interfaces.go @@ -0,0 +1,27 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gce + +import compute "google.golang.org/api/compute/v1" + +// CloudAddressService is an interface for managing addresses +type CloudAddressService interface { + ReserveRegionAddress(*compute.Address, string) error + GetRegionAddress(string, string) (*compute.Address, error) + // TODO: Mock `DeleteRegionAddress(name, region string) endpoint + // TODO: Mock Global endpoints +} diff --git a/pkg/cloudprovider/providers/gce/gce_loadbalancer_external.go b/pkg/cloudprovider/providers/gce/gce_loadbalancer_external.go index 49450db7612..7d78e59e74c 100644 --- a/pkg/cloudprovider/providers/gce/gce_loadbalancer_external.go +++ b/pkg/cloudprovider/providers/gce/gce_loadbalancer_external.go @@ -140,7 +140,7 @@ func (gce *GCECloud) ensureExternalLoadBalancer(clusterName, clusterID string, a // to this forwarding rule, so we can keep it. isUserOwnedIP = false isSafeToReleaseIP = true - ipAddress, _, err = gce.ensureStaticIP(loadBalancerName, serviceName.String(), gce.region, fwdRuleIP) + ipAddress, _, err = ensureStaticIP(gce, loadBalancerName, serviceName.String(), gce.region, fwdRuleIP) if err != nil { return nil, fmt.Errorf("failed to ensure static IP %s: %v", fwdRuleIP, err) } @@ -161,7 +161,7 @@ func (gce *GCECloud) ensureExternalLoadBalancer(clusterName, clusterID string, a // IP from ephemeral to static, or it will just get the IP if it is // already static. existed := false - ipAddress, existed, err = gce.ensureStaticIP(loadBalancerName, serviceName.String(), gce.region, fwdRuleIP) + ipAddress, existed, err = ensureStaticIP(gce, loadBalancerName, serviceName.String(), gce.region, fwdRuleIP) if err != nil { return nil, fmt.Errorf("failed to ensure static IP %s: %v", fwdRuleIP, err) } @@ -906,7 +906,7 @@ func (gce *GCECloud) projectOwnsStaticIP(name, region string, ipAddress string) return false, nil } -func (gce *GCECloud) ensureStaticIP(name, serviceName, region, existingIP string) (ipAddress string, created bool, err error) { +func ensureStaticIP(s CloudAddressService, name, serviceName, region, existingIP string) (ipAddress string, existing bool, err error) { // If the address doesn't exist, this will create it. // If the existingIP exists but is ephemeral, this will promote it to static. // If the address already exists, this will harmlessly return a StatusConflict @@ -921,7 +921,7 @@ func (gce *GCECloud) ensureStaticIP(name, serviceName, region, existingIP string addressObj.Address = existingIP } - if err = gce.ReserveRegionAddress(addressObj, region); err != nil { + if err = s.ReserveRegionAddress(addressObj, region); err != nil { if !isHTTPErrorCode(err, http.StatusConflict) { return "", false, fmt.Errorf("error creating gce static IP address: %v", err) } @@ -929,7 +929,7 @@ func (gce *GCECloud) ensureStaticIP(name, serviceName, region, existingIP string existed = true } - addr, err := gce.GetRegionAddress(name, region) + addr, err := s.GetRegionAddress(name, region) if err != nil { return "", false, fmt.Errorf("error getting static IP address: %v", err) } diff --git a/pkg/cloudprovider/providers/gce/gce_loadbalancer_external_test.go b/pkg/cloudprovider/providers/gce/gce_loadbalancer_external_test.go new file mode 100644 index 00000000000..0dff1a2945a --- /dev/null +++ b/pkg/cloudprovider/providers/gce/gce_loadbalancer_external_test.go @@ -0,0 +1,39 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gce + +import "testing" + +func TestEnsureStaticIP(t *testing.T) { + fcas := NewFakeCloudAddressService() + ipName := "some-static-ip" + serviceName := "" + region := "us-central1" + + // First ensure call + ip, existed, err := ensureStaticIP(fcas, ipName, serviceName, region, "") + if err != nil || existed || ip == "" { + t.Fatalf(`ensureStaticIP(%v, %v, %v, %v, "") = %v, %v, %v; want valid ip, false, nil`, fcas, ipName, serviceName, region, ip, existed, err) + } + + // Second ensure call + var ipPrime string + ipPrime, existed, err = ensureStaticIP(fcas, ipName, serviceName, region, ip) + if err != nil || !existed || ip != ipPrime { + t.Fatalf(`ensureStaticIP(%v, %v, %v, %v, %v) = %v, %v, %v; want %v, true, nil`, fcas, ipName, serviceName, region, ip, ipPrime, existed, err, ip) + } +}