Commit 76945ad8 authored by Yu-Ju Hong's avatar Yu-Ju Hong

GCE: Add "Network Tiers" as an Alpha feature for L4 load balancers

This feature supports specifying what network tier (premium, standard) to use for the load balancer backing the Service (type=LoadBalancer).
parent 92db97df
...@@ -176,3 +176,15 @@ func (gce *GCECloud) GetBetaRegionAddressByIP(region, ipAddress string) (*comput ...@@ -176,3 +176,15 @@ func (gce *GCECloud) GetBetaRegionAddressByIP(region, ipAddress string) (*comput
} }
return nil, makeGoogleAPINotFoundError(fmt.Sprintf("Address with IP %q was not found in region %q", ipAddress, region)) return nil, makeGoogleAPINotFoundError(fmt.Sprintf("Address with IP %q was not found in region %q", ipAddress, region))
} }
// TODO(#51665): retire this function once Network Tiers becomes Beta in GCP.
func (gce *GCECloud) getNetworkTierFromAddress(name, region string) (string, error) {
if !gce.AlphaFeatureGate.Enabled(AlphaFeatureNetworkTiers) {
return NetworkTierDefault.ToGCEValue(), nil
}
addr, err := gce.GetAlphaRegionAddress(name, region)
if err != nil {
return handleAlphaNetworkTierGetError(err)
}
return addr.NetworkTier, nil
}
...@@ -27,6 +27,8 @@ import ( ...@@ -27,6 +27,8 @@ import (
compute "google.golang.org/api/compute/v1" compute "google.golang.org/api/compute/v1"
) )
// test
type FakeCloudAddressService struct { type FakeCloudAddressService struct {
count int count int
// reservedAddrs tracks usage of IP addresses // reservedAddrs tracks usage of IP addresses
...@@ -70,7 +72,16 @@ func (cas *FakeCloudAddressService) ReserveAlphaRegionAddress(addr *computealpha ...@@ -70,7 +72,16 @@ func (cas *FakeCloudAddressService) ReserveAlphaRegionAddress(addr *computealpha
} }
if cas.reservedAddrs[addr.Address] { if cas.reservedAddrs[addr.Address] {
return makeGoogleAPIError(http.StatusConflict, "IP in use") msg := "IP in use"
// When the IP is already in use, this call returns an error code based
// on the type (internal vs external) of the address. This is to be
// consistent with actual GCE API.
switch lbScheme(addr.AddressType) {
case schemeExternal:
return makeGoogleAPIError(http.StatusBadRequest, msg)
default:
return makeGoogleAPIError(http.StatusConflict, msg)
}
} }
if _, exists := cas.addrsByRegionAndName[region]; !exists { if _, exists := cas.addrsByRegionAndName[region]; !exists {
...@@ -133,6 +144,7 @@ func (cas *FakeCloudAddressService) DeleteRegionAddress(name, region string) err ...@@ -133,6 +144,7 @@ func (cas *FakeCloudAddressService) DeleteRegionAddress(name, region string) err
if !exists { if !exists {
return makeGoogleAPINotFoundError("") return makeGoogleAPINotFoundError("")
} }
delete(cas.reservedAddrs, addr.Address) delete(cas.reservedAddrs, addr.Address)
delete(cas.addrsByRegionAndName[region], name) delete(cas.addrsByRegionAndName[region], name)
return nil return nil
...@@ -167,6 +179,14 @@ func (cas *FakeCloudAddressService) GetRegionAddressByIP(name, region string) (* ...@@ -167,6 +179,14 @@ func (cas *FakeCloudAddressService) GetRegionAddressByIP(name, region string) (*
return nil, err return nil, err
} }
func (cas *FakeCloudAddressService) getNetworkTierFromAddress(name, region string) (string, error) {
addr, err := cas.GetAlphaRegionAddress(name, region)
if err != nil {
return "", err
}
return addr.NetworkTier, nil
}
func convertToV1Address(object gceObject) *compute.Address { func convertToV1Address(object gceObject) *compute.Address {
enc, err := object.MarshalJSON() enc, err := object.MarshalJSON()
if err != nil { if err != nil {
...@@ -188,6 +208,8 @@ func convertToAlphaAddress(object gceObject) *computealpha.Address { ...@@ -188,6 +208,8 @@ func convertToAlphaAddress(object gceObject) *computealpha.Address {
if err := json.Unmarshal(enc, &addr); err != nil { if err := json.Unmarshal(enc, &addr); err != nil {
panic(fmt.Sprintf("Failed to convert GCE apiObject %v to alpha address: %v", object, err)) panic(fmt.Sprintf("Failed to convert GCE apiObject %v to alpha address: %v", object, err))
} }
// Set the default values for the Alpha fields.
addr.NetworkTier = NetworkTierDefault.ToGCEValue()
return &addr return &addr
} }
......
...@@ -22,15 +22,22 @@ import ( ...@@ -22,15 +22,22 @@ import (
utilerrors "k8s.io/apimachinery/pkg/util/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors"
) )
const (
// alpha: v1.8 (for Services)
//
// Allows Services backed by a GCP load balancer to choose what network
// tier to use. Currently supports "Standard" and "Premium" (default).
AlphaFeatureNetworkTiers = "NetworkTiers"
GCEDiskAlphaFeatureGate = "GCEDiskAlphaAPI"
)
// All known alpha features // All known alpha features
var knownAlphaFeatures = map[string]bool{ var knownAlphaFeatures = map[string]bool{
AlphaFeatureNetworkTiers: true,
GCEDiskAlphaFeatureGate: true, GCEDiskAlphaFeatureGate: true,
} }
const (
GCEDiskAlphaFeatureGate = "GCEDiskAlphaAPI"
)
type AlphaFeatureGate struct { type AlphaFeatureGate struct {
features map[string]bool features map[string]bool
} }
......
...@@ -141,3 +141,15 @@ func (gce *GCECloud) DeleteRegionForwardingRule(name, region string) error { ...@@ -141,3 +141,15 @@ func (gce *GCECloud) DeleteRegionForwardingRule(name, region string) error {
return gce.waitForRegionOp(op, region, mc) return gce.waitForRegionOp(op, region, mc)
} }
// TODO(#51665): retire this function once Network Tiers becomes Beta in GCP.
func (gce *GCECloud) getNetworkTierFromForwardingRule(name, region string) (string, error) {
if !gce.AlphaFeatureGate.Enabled(AlphaFeatureNetworkTiers) {
return NetworkTierDefault.ToGCEValue(), nil
}
fwdRule, err := gce.GetAlphaRegionForwardingRule(name, region)
if err != nil {
return handleAlphaNetworkTierGetError(err)
}
return fwdRule.NetworkTier, nil
}
...@@ -102,6 +102,14 @@ func (f *FakeCloudForwardingRuleService) GetRegionForwardingRule(name, region st ...@@ -102,6 +102,14 @@ func (f *FakeCloudForwardingRuleService) GetRegionForwardingRule(name, region st
return nil, err return nil, err
} }
func (f *FakeCloudForwardingRuleService) getNetworkTierFromForwardingRule(name, region string) (string, error) {
fwdRule, err := f.GetAlphaRegionForwardingRule(name, region)
if err != nil {
return "", err
}
return fwdRule.NetworkTier, nil
}
func convertToV1ForwardingRule(object gceObject) *compute.ForwardingRule { func convertToV1ForwardingRule(object gceObject) *compute.ForwardingRule {
enc, err := object.MarshalJSON() enc, err := object.MarshalJSON()
if err != nil { if err != nil {
...@@ -123,5 +131,8 @@ func convertToAlphaForwardingRule(object gceObject) *computealpha.ForwardingRule ...@@ -123,5 +131,8 @@ func convertToAlphaForwardingRule(object gceObject) *computealpha.ForwardingRule
if err := json.Unmarshal(enc, &fwdRule); err != nil { if err := json.Unmarshal(enc, &fwdRule); err != nil {
panic(fmt.Sprintf("Failed to convert GCE apiObject %v to alpha fwdRuleess: %v", object, err)) panic(fmt.Sprintf("Failed to convert GCE apiObject %v to alpha fwdRuleess: %v", object, err))
} }
// Set the default values for the Alpha fields.
fwdRule.NetworkTier = NetworkTierDefault.ToGCEValue()
return &fwdRule return &fwdRule
} }
...@@ -22,6 +22,8 @@ import ( ...@@ -22,6 +22,8 @@ import (
compute "google.golang.org/api/compute/v1" compute "google.golang.org/api/compute/v1"
) )
// These interfaces are added for testability.
// CloudAddressService is an interface for managing addresses // CloudAddressService is an interface for managing addresses
type CloudAddressService interface { type CloudAddressService interface {
ReserveRegionAddress(address *compute.Address, region string) error ReserveRegionAddress(address *compute.Address, region string) error
...@@ -38,6 +40,9 @@ type CloudAddressService interface { ...@@ -38,6 +40,9 @@ type CloudAddressService interface {
ReserveBetaRegionAddress(address *computebeta.Address, region string) error ReserveBetaRegionAddress(address *computebeta.Address, region string) error
GetBetaRegionAddress(name string, region string) (*computebeta.Address, error) GetBetaRegionAddress(name string, region string) (*computebeta.Address, error)
GetBetaRegionAddressByIP(region, ipAddress string) (*computebeta.Address, error) GetBetaRegionAddressByIP(region, ipAddress string) (*computebeta.Address, error)
// TODO(#51665): Remove this once the Network Tiers becomes Alpha in GCP.
getNetworkTierFromAddress(name, region string) (string, error)
} }
// CloudForwardingRuleService is an interface for managing forwarding rules. // CloudForwardingRuleService is an interface for managing forwarding rules.
...@@ -50,4 +55,7 @@ type CloudForwardingRuleService interface { ...@@ -50,4 +55,7 @@ type CloudForwardingRuleService interface {
// Alpha API. // Alpha API.
GetAlphaRegionForwardingRule(name, region string) (*computealpha.ForwardingRule, error) GetAlphaRegionForwardingRule(name, region string) (*computealpha.ForwardingRule, error)
CreateAlphaRegionForwardingRule(rule *computealpha.ForwardingRule, region string) error CreateAlphaRegionForwardingRule(rule *computealpha.ForwardingRule, region string) error
// Needed for the Alpha "Network Tiers" feature.
getNetworkTierFromForwardingRule(name, region string) (string, error)
} }
...@@ -21,8 +21,10 @@ import ( ...@@ -21,8 +21,10 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
computealpha "google.golang.org/api/compute/v0.alpha" computealpha "google.golang.org/api/compute/v0.alpha"
"k8s.io/api/core/v1"
) )
func TestEnsureStaticIP(t *testing.T) { func TestEnsureStaticIP(t *testing.T) {
...@@ -32,19 +34,54 @@ func TestEnsureStaticIP(t *testing.T) { ...@@ -32,19 +34,54 @@ func TestEnsureStaticIP(t *testing.T) {
region := "us-central1" region := "us-central1"
// First ensure call // First ensure call
ip, existed, err := ensureStaticIP(fcas, ipName, serviceName, region, "") ip, existed, err := ensureStaticIP(fcas, ipName, serviceName, region, "", NetworkTierDefault)
if err != nil || existed || ip == "" { 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) 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 // Second ensure call
var ipPrime string var ipPrime string
ipPrime, existed, err = ensureStaticIP(fcas, ipName, serviceName, region, ip) ipPrime, existed, err = ensureStaticIP(fcas, ipName, serviceName, region, ip, NetworkTierDefault)
if err != nil || !existed || ip != ipPrime { 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) t.Fatalf(`ensureStaticIP(%v, %v, %v, %v, %v) = %v, %v, %v; want %v, true, nil`, fcas, ipName, serviceName, region, ip, ipPrime, existed, err, ip)
} }
} }
func TestEnsureStaticIPWithTier(t *testing.T) {
s := NewFakeCloudAddressService()
serviceName := ""
region := "us-east1"
for desc, tc := range map[string]struct {
name string
netTier NetworkTier
expected string
}{
"Premium (default)": {
name: "foo-1",
netTier: NetworkTierPremium,
expected: "PREMIUM",
},
"Standard": {
name: "foo-2",
netTier: NetworkTierStandard,
expected: "STANDARD",
},
} {
t.Run(desc, func(t *testing.T) {
ip, existed, err := ensureStaticIP(s, tc.name, serviceName, region, "", tc.netTier)
assert.NoError(t, err)
assert.False(t, existed)
assert.NotEqual(t, "", ip)
// Get the Address from the fake address service and verify that the tier
// is set correctly.
alphaAddr, err := s.GetAlphaRegionAddress(tc.name, region)
require.NoError(t, err)
assert.Equal(t, tc.expected, alphaAddr.NetworkTier)
})
}
}
func TestVerifyRequestedIP(t *testing.T) { func TestVerifyRequestedIP(t *testing.T) {
region := "test-region" region := "test-region"
lbRef := "test-lb" lbRef := "test-lb"
...@@ -53,35 +90,150 @@ func TestVerifyRequestedIP(t *testing.T) { ...@@ -53,35 +90,150 @@ func TestVerifyRequestedIP(t *testing.T) {
for desc, tc := range map[string]struct { for desc, tc := range map[string]struct {
requestedIP string requestedIP string
fwdRuleIP string fwdRuleIP string
netTier NetworkTier
addrList []*computealpha.Address addrList []*computealpha.Address
expectErr bool expectErr bool
expectUserOwned bool expectUserOwned bool
}{ }{
"requested IP exists": { "requested IP exists": {
requestedIP: "1.1.1.1", requestedIP: "1.1.1.1",
addrList: []*computealpha.Address{{Name: "foo", Address: "1.1.1.1"}}, netTier: NetworkTierPremium,
addrList: []*computealpha.Address{{Name: "foo", Address: "1.1.1.1", NetworkTier: "PREMIUM"}},
expectErr: false, expectErr: false,
expectUserOwned: true, expectUserOwned: true,
}, },
"requested IP is not static, but is in use by the fwd rule": { "requested IP is not static, but is in use by the fwd rule": {
requestedIP: "1.1.1.1", requestedIP: "1.1.1.1",
fwdRuleIP: "1.1.1.1", fwdRuleIP: "1.1.1.1",
netTier: NetworkTierPremium,
expectErr: false, expectErr: false,
}, },
"requested IP is not static and is not used by the fwd rule": { "requested IP is not static and is not used by the fwd rule": {
requestedIP: "1.1.1.1", requestedIP: "1.1.1.1",
fwdRuleIP: "2.2.2.2", fwdRuleIP: "2.2.2.2",
netTier: NetworkTierPremium,
expectErr: true, expectErr: true,
}, },
"no requested IP": { "no requested IP": {
netTier: NetworkTierPremium,
expectErr: false, expectErr: false,
}, },
"requested IP exists, but network tier does not match": {
requestedIP: "1.1.1.1",
netTier: NetworkTierStandard,
addrList: []*computealpha.Address{{Name: "foo", Address: "1.1.1.1", NetworkTier: "PREMIUM"}},
expectErr: true,
},
} { } {
t.Run(desc, func(t *testing.T) { t.Run(desc, func(t *testing.T) {
s.SetRegionalAddresses(region, tc.addrList) s.SetRegionalAddresses(region, tc.addrList)
isUserOwnedIP, err := verifyUserRequestedIP(s, region, tc.requestedIP, tc.fwdRuleIP, lbRef) isUserOwnedIP, err := verifyUserRequestedIP(s, region, tc.requestedIP, tc.fwdRuleIP, lbRef, tc.netTier)
assert.Equal(t, tc.expectErr, err != nil, fmt.Sprintf("err: %v", err)) assert.Equal(t, tc.expectErr, err != nil, fmt.Sprintf("err: %v", err))
assert.Equal(t, tc.expectUserOwned, isUserOwnedIP, desc) assert.Equal(t, tc.expectUserOwned, isUserOwnedIP)
})
}
}
func TestCreateForwardingRuleWithTier(t *testing.T) {
s := NewFakeCloudForwardingRuleService()
// Common variables among the tests.
ports := []v1.ServicePort{{Name: "foo", Protocol: v1.ProtocolTCP, Port: int32(123)}}
region := "test-region"
target := "test-target-pool"
svcName := "foo-svc"
for desc, tc := range map[string]struct {
netTier NetworkTier
expectedRule *computealpha.ForwardingRule
}{
"Premium tier": {
netTier: NetworkTierPremium,
expectedRule: &computealpha.ForwardingRule{
Name: "lb-1",
Description: `{"kubernetes.io/service-name":"foo-svc"}`,
IPAddress: "1.1.1.1",
IPProtocol: "TCP",
PortRange: "123-123",
Target: target,
NetworkTier: "PREMIUM",
},
},
"Standard tier": {
netTier: NetworkTierStandard,
expectedRule: &computealpha.ForwardingRule{
Name: "lb-2",
Description: `{"kubernetes.io/service-name":"foo-svc"}`,
IPAddress: "2.2.2.2",
IPProtocol: "TCP",
PortRange: "123-123",
Target: target,
NetworkTier: "STANDARD",
},
},
} {
t.Run(desc, func(t *testing.T) {
lbName := tc.expectedRule.Name
ipAddr := tc.expectedRule.IPAddress
err := createForwardingRule(s, lbName, svcName, region, ipAddr, target, ports, tc.netTier)
assert.NoError(t, err)
alphaRule, err := s.GetAlphaRegionForwardingRule(lbName, region)
assert.NoError(t, err)
assert.Equal(t, tc.expectedRule, alphaRule)
})
}
}
func TestDeleteAddressWithWrongTier(t *testing.T) {
region := "test-region"
lbRef := "test-lb"
s := NewFakeCloudAddressService()
for desc, tc := range map[string]struct {
addrName string
netTier NetworkTier
addrList []*computealpha.Address
expectDelete bool
}{
"Network tiers (premium) match; do nothing": {
addrName: "foo1",
netTier: NetworkTierPremium,
addrList: []*computealpha.Address{{Name: "foo1", Address: "1.1.1.1", NetworkTier: "PREMIUM"}},
},
"Network tiers (standard) match; do nothing": {
addrName: "foo2",
netTier: NetworkTierStandard,
addrList: []*computealpha.Address{{Name: "foo2", Address: "1.1.1.2", NetworkTier: "STANDARD"}},
},
"Wrong network tier (standard); delete address": {
addrName: "foo3",
netTier: NetworkTierPremium,
addrList: []*computealpha.Address{{Name: "foo3", Address: "1.1.1.3", NetworkTier: "STANDARD"}},
expectDelete: true,
},
"Wrong network tier (preimium); delete address": {
addrName: "foo4",
netTier: NetworkTierStandard,
addrList: []*computealpha.Address{{Name: "foo4", Address: "1.1.1.4", NetworkTier: "PREMIUM"}},
expectDelete: true,
},
} {
t.Run(desc, func(t *testing.T) {
s.SetRegionalAddresses(region, tc.addrList)
// Sanity check to ensure we inject the right address.
_, err := s.GetRegionAddress(tc.addrName, region)
require.NoError(t, err)
err = deleteAddressWithWrongTier(s, region, tc.addrName, lbRef, tc.netTier)
assert.NoError(t, err)
// Check whether the address still exists.
_, err = s.GetRegionAddress(tc.addrName, region)
if tc.expectDelete {
assert.True(t, isNotFound(err))
} else {
assert.NoError(t, err)
}
}) })
} }
} }
...@@ -84,6 +84,11 @@ func makeBackendServiceDescription(nm types.NamespacedName, shared bool) string ...@@ -84,6 +84,11 @@ func makeBackendServiceDescription(nm types.NamespacedName, shared bool) string
// External Load Balancer // External Load Balancer
// makeServiceDescription is used to generate descriptions for forwarding rules and addresses.
func makeServiceDescription(serviceName string) string {
return fmt.Sprintf(`{"kubernetes.io/service-name":"%s"}`, serviceName)
}
// makeNodesHealthCheckName returns name of the health check resource used by // makeNodesHealthCheckName returns name of the health check resource used by
// the GCE load balancers (l4) for performing health checks on nodes. // the GCE load balancers (l4) for performing health checks on nodes.
func makeNodesHealthCheckName(clusterID string) string { func makeNodesHealthCheckName(clusterID string) string {
......
...@@ -157,3 +157,19 @@ func makeGoogleAPINotFoundError(message string) error { ...@@ -157,3 +157,19 @@ func makeGoogleAPINotFoundError(message string) error {
func makeGoogleAPIError(code int, message string) error { func makeGoogleAPIError(code int, message string) error {
return &googleapi.Error{Code: code, Message: message} return &googleapi.Error{Code: code, Message: message}
} }
func isForbidden(err error) bool {
return isHTTPErrorCode(err, http.StatusForbidden)
}
// TODO(#51665): Remove this once Network Tiers becomes Beta in GCP.
func handleAlphaNetworkTierGetError(err error) (string, error) {
if isForbidden(err) {
// Network tier is still an Alpha feature in GCP, and not every project
// is whitelisted to access the API. If we cannot access the API, just
// assume the tier is premium.
return NetworkTierDefault.ToGCEValue(), nil
}
// Can't get the network tier, just return an error.
return "", err
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment