Commit b82c028f authored by Girish Kalele's avatar Girish Kalele

GCE Cloud provider changes for ESIPP

Add feature gate (ExternalTrafficLocalOnly) for alpha feature
parent 4b554925
...@@ -16,6 +16,13 @@ limitations under the License. ...@@ -16,6 +16,13 @@ limitations under the License.
package service package service
import (
"strconv"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/api"
)
const ( const (
// AnnotationLoadBalancerSourceRangesKey is the key of the annotation on a service to set allowed ingress ranges on their LoadBalancers // AnnotationLoadBalancerSourceRangesKey is the key of the annotation on a service to set allowed ingress ranges on their LoadBalancers
// //
...@@ -25,4 +32,58 @@ const ( ...@@ -25,4 +32,58 @@ const (
// //
// Not all cloud providers support this annotation, though AWS & GCE do. // Not all cloud providers support this annotation, though AWS & GCE do.
AnnotationLoadBalancerSourceRangesKey = "service.beta.kubernetes.io/load-balancer-source-ranges" AnnotationLoadBalancerSourceRangesKey = "service.beta.kubernetes.io/load-balancer-source-ranges"
// AnnotationExternalTraffic An annotation that denotes if this Service desires to route external traffic to local
// endpoints only. This preserves Source IP and avoids a second hop.
AnnotationExternalTraffic = "service.alpha.kubernetes.io/external-traffic"
// AnnotationValueExternalTrafficLocal Value of annotation to specify local endpoints behaviour
AnnotationValueExternalTrafficLocal = "OnlyLocal"
// AnnotationValueExternalTrafficGlobal Value of annotation to specify global (legacy) behaviour
AnnotationValueExternalTrafficGlobal = "Global"
// AnnotationHealthCheckNodePort Annotation specifying the healthcheck nodePort for the service
// If not specified, annotation is created by the service api backend with the allocated nodePort
// Will use user-specified nodePort value if specified by the client
AnnotationHealthCheckNodePort = "service.alpha.kubernetes.io/healthcheck-nodeport"
) )
// NeedsHealthCheck Check service for health check annotations
func NeedsHealthCheck(service *api.Service) bool {
if l, ok := service.Annotations[AnnotationExternalTraffic]; ok {
if l == AnnotationValueExternalTrafficLocal {
return true
} else if l == AnnotationValueExternalTrafficGlobal {
return false
} else {
glog.Errorf("Invalid value for annotation %v", AnnotationExternalTraffic)
return false
}
}
return false
}
// GetServiceHealthCheckNodePort Return health check node port annotation for service, if one exists
func GetServiceHealthCheckNodePort(service *api.Service) int32 {
if NeedsHealthCheck(service) {
if l, ok := service.Annotations[AnnotationHealthCheckNodePort]; ok {
p, err := strconv.Atoi(l)
if err != nil {
glog.Errorf("Failed to parse annotation %v: %v", AnnotationHealthCheckNodePort, err)
return 0
}
return int32(p)
}
}
return 0
}
// GetServiceHealthCheckPathPort Return the path and nodePort programmed into the Cloud LB Health Check
func GetServiceHealthCheckPathPort(service *api.Service) (string, int32) {
if !NeedsHealthCheck(service) {
return "", 0
}
port := GetServiceHealthCheckNodePort(service)
if port == 0 {
return "", 0
}
return "/healthz", port
}
...@@ -31,7 +31,7 @@ import ( ...@@ -31,7 +31,7 @@ import (
"gopkg.in/gcfg.v1" "gopkg.in/gcfg.v1"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/service" apiservice "k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/cloudprovider" "k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/types" "k8s.io/kubernetes/pkg/types"
...@@ -70,8 +70,16 @@ const ( ...@@ -70,8 +70,16 @@ const (
// were to continuously return a nextPageToken. // were to continuously return a nextPageToken.
maxPages = 25 maxPages = 25
// Target Pool creation is limited to 200 instances.
maxTargetPoolCreateInstances = 200 maxTargetPoolCreateInstances = 200
// HTTP Load Balancer parameters
// Configure 2 second period for external health checks.
gceHcCheckIntervalSeconds = int64(2)
gceHcTimeoutSeconds = int64(1)
// Start sending requests as soon as a pod is found on the node.
gceHcHealthyThreshold = int64(1)
// Defaults to 5 * 2 = 10 seconds before the LB will steer traffic away
gceHcUnhealthyThreshold = int64(5)
) )
// GCECloud is an implementation of Interface, LoadBalancer and Instances for Google Compute Engine. // GCECloud is an implementation of Interface, LoadBalancer and Instances for Google Compute Engine.
...@@ -665,7 +673,7 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv ...@@ -665,7 +673,7 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv
// is because the forwarding rule is used as the indicator that the load // is because the forwarding rule is used as the indicator that the load
// balancer is fully created - it's what getLoadBalancer checks for. // balancer is fully created - it's what getLoadBalancer checks for.
// Check if user specified the allow source range // Check if user specified the allow source range
sourceRanges, err := service.GetLoadBalancerSourceRanges(apiService) sourceRanges, err := apiservice.GetLoadBalancerSourceRanges(apiService)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -720,7 +728,18 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv ...@@ -720,7 +728,18 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv
glog.Infof("EnsureLoadBalancer(%v(%v)): deleted forwarding rule", loadBalancerName, serviceName) glog.Infof("EnsureLoadBalancer(%v(%v)): deleted forwarding rule", loadBalancerName, serviceName)
} }
if tpExists && tpNeedsUpdate { if tpExists && tpNeedsUpdate {
if err := gce.deleteTargetPool(loadBalancerName, gce.region); err != nil { // Generate the list of health checks for this target pool to pass to deleteTargetPool
var hc *compute.HttpHealthCheck
if path, _ := apiservice.GetServiceHealthCheckPathPort(apiService); path != "" {
var err error
hc, err = gce.GetHttpHealthCheck(loadBalancerName)
if err != nil && !isHTTPErrorCode(err, http.StatusNotFound) {
glog.Infof("Failed to retrieve health check %v:%v", loadBalancerName, err)
}
}
// Pass healthchecks to deleteTargetPool to cleanup health checks prior to cleaning up the target pool itself.
if err := gce.deleteTargetPool(loadBalancerName, gce.region, hc); err != nil {
return nil, fmt.Errorf("failed to delete existing target pool %s for load balancer update: %v", loadBalancerName, err) return nil, fmt.Errorf("failed to delete existing target pool %s for load balancer update: %v", loadBalancerName, err)
} }
glog.Infof("EnsureLoadBalancer(%v(%v)): deleted target pool", loadBalancerName, serviceName) glog.Infof("EnsureLoadBalancer(%v(%v)): deleted target pool", loadBalancerName, serviceName)
...@@ -734,7 +753,18 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv ...@@ -734,7 +753,18 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv
createInstances = createInstances[:maxTargetPoolCreateInstances] createInstances = createInstances[:maxTargetPoolCreateInstances]
} }
if err := gce.createTargetPool(loadBalancerName, serviceName.String(), gce.region, createInstances, affinityType); err != nil { // Create health checks for this target pool to pass to createTargetPool for health check links
var hc *compute.HttpHealthCheck
if path, healthCheckNodePort := apiservice.GetServiceHealthCheckPathPort(apiService); path != "" {
glog.Infof("service %v needs health checks on :%d/%s)", apiService.Name, healthCheckNodePort, path)
var err error
hc, err = gce.ensureHttpHealthCheck(loadBalancerName, path, healthCheckNodePort)
if err != nil {
return nil, fmt.Errorf("Failed to create health check for localized service %v on node port %v: %v", loadBalancerName, healthCheckNodePort, err)
}
}
// Pass healthchecks to createTargetPool which needs them as health check links in the target pool
if err := gce.createTargetPool(loadBalancerName, serviceName.String(), gce.region, createInstances, affinityType, hc); err != nil {
return nil, fmt.Errorf("failed to create target pool %s: %v", loadBalancerName, err) return nil, fmt.Errorf("failed to create target pool %s: %v", loadBalancerName, err)
} }
if len(hosts) <= maxTargetPoolCreateInstances { if len(hosts) <= maxTargetPoolCreateInstances {
...@@ -767,9 +797,53 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv ...@@ -767,9 +797,53 @@ func (gce *GCECloud) EnsureLoadBalancer(clusterName string, apiService *api.Serv
status := &api.LoadBalancerStatus{} status := &api.LoadBalancerStatus{}
status.Ingress = []api.LoadBalancerIngress{{IP: ipAddress}} status.Ingress = []api.LoadBalancerIngress{{IP: ipAddress}}
return status, nil return status, nil
} }
func makeHealthCheckDescription(serviceName string) string {
return fmt.Sprintf(`{"kubernetes.io/service-name":"%s"}`, serviceName)
}
func (gce *GCECloud) ensureHttpHealthCheck(name, path string, port int32) (hc *compute.HttpHealthCheck, err error) {
newHC := &compute.HttpHealthCheck{
Name: name,
Port: int64(port),
RequestPath: path,
Host: "",
Description: makeHealthCheckDescription(name),
CheckIntervalSec: gceHcCheckIntervalSeconds,
TimeoutSec: gceHcTimeoutSeconds,
HealthyThreshold: gceHcHealthyThreshold,
UnhealthyThreshold: gceHcUnhealthyThreshold,
}
hc, err = gce.GetHttpHealthCheck(name)
if hc == nil || err != nil && isHTTPErrorCode(err, http.StatusNotFound) {
glog.Infof("Did not find health check %v, creating port %v path %v", name, port, path)
if err = gce.CreateHttpHealthCheck(newHC); err != nil {
return nil, err
}
hc, err = gce.GetHttpHealthCheck(name)
if err != nil {
glog.Errorf("Failed to get http health check %v", err)
return nil, err
}
return hc, nil
}
// Validate health check fields
drift := hc.Port != int64(port) || hc.RequestPath != path || hc.Description != makeHealthCheckDescription(name)
drift = drift || hc.CheckIntervalSec != gceHcCheckIntervalSeconds || hc.TimeoutSec != gceHcTimeoutSeconds
drift = drift || hc.UnhealthyThreshold != gceHcUnhealthyThreshold || hc.HealthyThreshold != gceHcHealthyThreshold
if drift {
glog.Infof("Health check %v exists but parameters have drifted - updating", name)
if err := gce.UpdateHttpHealthCheck(newHC); err != nil {
return nil, err
}
}
return hc, nil
}
// Passing nil for requested IP is perfectly fine - it just means that no specific // Passing nil for requested IP is perfectly fine - it just means that no specific
// IP is being requested. // IP is being requested.
// Returns whether the forwarding rule exists, whether it needs to be updated, // Returns whether the forwarding rule exists, whether it needs to be updated,
...@@ -962,16 +1036,24 @@ func (gce *GCECloud) createForwardingRule(name, serviceName, region, ipAddress s ...@@ -962,16 +1036,24 @@ func (gce *GCECloud) createForwardingRule(name, serviceName, region, ipAddress s
return nil return nil
} }
func (gce *GCECloud) createTargetPool(name, serviceName, region string, hosts []*gceInstance, affinityType api.ServiceAffinity) error { func (gce *GCECloud) createTargetPool(name, serviceName, region string, hosts []*gceInstance, affinityType api.ServiceAffinity, hc *compute.HttpHealthCheck) error {
var instances []string var instances []string
for _, host := range hosts { for _, host := range hosts {
instances = append(instances, makeHostURL(gce.projectID, host.Zone, host.Name)) instances = append(instances, makeHostURL(gce.projectID, host.Zone, host.Name))
} }
hcLinks := []string{}
if hc != nil {
hcLinks = append(hcLinks, hc.SelfLink)
}
if len(hcLinks) > 0 {
glog.Infof("Creating targetpool %v with healthchecking", name)
}
pool := &compute.TargetPool{ pool := &compute.TargetPool{
Name: name, Name: name,
Description: fmt.Sprintf(`{"kubernetes.io/service-name":"%s"}`, serviceName), Description: fmt.Sprintf(`{"kubernetes.io/service-name":"%s"}`, serviceName),
Instances: instances, Instances: instances,
SessionAffinity: translateAffinityType(affinityType), SessionAffinity: translateAffinityType(affinityType),
HealthChecks: hcLinks,
} }
op, err := gce.service.TargetPools.Insert(gce.projectID, region, pool).Do() op, err := gce.service.TargetPools.Insert(gce.projectID, region, pool).Do()
if err != nil && !isHTTPErrorCode(err, http.StatusConflict) { if err != nil && !isHTTPErrorCode(err, http.StatusConflict) {
...@@ -1279,6 +1361,16 @@ func (gce *GCECloud) EnsureLoadBalancerDeleted(clusterName string, service *api. ...@@ -1279,6 +1361,16 @@ func (gce *GCECloud) EnsureLoadBalancerDeleted(clusterName string, service *api.
glog.V(2).Infof("EnsureLoadBalancerDeleted(%v, %v, %v, %v, %v)", clusterName, service.Namespace, service.Name, loadBalancerName, glog.V(2).Infof("EnsureLoadBalancerDeleted(%v, %v, %v, %v, %v)", clusterName, service.Namespace, service.Name, loadBalancerName,
gce.region) gce.region)
var hc *compute.HttpHealthCheck
if path, _ := apiservice.GetServiceHealthCheckPathPort(service); path != "" {
var err error
hc, err = gce.GetHttpHealthCheck(loadBalancerName)
if err != nil && !isHTTPErrorCode(err, http.StatusNotFound) {
glog.Infof("Failed to retrieve health check %v:%v", loadBalancerName, err)
return err
}
}
errs := utilerrors.AggregateGoroutines( errs := utilerrors.AggregateGoroutines(
func() error { return gce.deleteFirewall(loadBalancerName, gce.region) }, func() error { return gce.deleteFirewall(loadBalancerName, gce.region) },
// Even though we don't hold on to static IPs for load balancers, it's // Even though we don't hold on to static IPs for load balancers, it's
...@@ -1291,7 +1383,7 @@ func (gce *GCECloud) EnsureLoadBalancerDeleted(clusterName string, service *api. ...@@ -1291,7 +1383,7 @@ func (gce *GCECloud) EnsureLoadBalancerDeleted(clusterName string, service *api.
if err := gce.deleteForwardingRule(loadBalancerName, gce.region); err != nil { if err := gce.deleteForwardingRule(loadBalancerName, gce.region); err != nil {
return err return err
} }
if err := gce.deleteTargetPool(loadBalancerName, gce.region); err != nil { if err := gce.deleteTargetPool(loadBalancerName, gce.region, hc); err != nil {
return err return err
} }
return nil return nil
...@@ -1319,7 +1411,14 @@ func (gce *GCECloud) deleteForwardingRule(name, region string) error { ...@@ -1319,7 +1411,14 @@ func (gce *GCECloud) deleteForwardingRule(name, region string) error {
return nil return nil
} }
func (gce *GCECloud) deleteTargetPool(name, region string) error { func (gce *GCECloud) deleteTargetPool(name, region string, hc *compute.HttpHealthCheck) error {
if hc != nil {
glog.Infof("Deleting health check %v", hc.Name)
if err := gce.DeleteHttpHealthCheck(hc.Name); err != nil {
glog.Warningf("Failed to delete health check %v: %v", hc, err)
return err
}
}
op, err := gce.service.TargetPools.Delete(gce.projectID, region, name).Do() op, err := gce.service.TargetPools.Delete(gce.projectID, region, name).Do()
if err != nil && isHTTPErrorCode(err, http.StatusNotFound) { if err != nil && isHTTPErrorCode(err, http.StatusNotFound) {
glog.Infof("Target pool %s already deleted. Continuing to delete other resources.", name) glog.Infof("Target pool %s already deleted. Continuing to delete other resources.", name)
......
...@@ -28,12 +28,14 @@ import ( ...@@ -28,12 +28,14 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/rest" "k8s.io/kubernetes/pkg/api/rest"
apiservice "k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/api/unversioned" "k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/api/validation"
"k8s.io/kubernetes/pkg/registry/endpoint" "k8s.io/kubernetes/pkg/registry/endpoint"
"k8s.io/kubernetes/pkg/registry/service/ipallocator" "k8s.io/kubernetes/pkg/registry/service/ipallocator"
"k8s.io/kubernetes/pkg/registry/service/portallocator" "k8s.io/kubernetes/pkg/registry/service/portallocator"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
featuregate "k8s.io/kubernetes/pkg/util/config"
utilnet "k8s.io/kubernetes/pkg/util/net" utilnet "k8s.io/kubernetes/pkg/util/net"
"k8s.io/kubernetes/pkg/util/validation/field" "k8s.io/kubernetes/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/watch" "k8s.io/kubernetes/pkg/watch"
...@@ -133,6 +135,35 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err ...@@ -133,6 +135,35 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err
} }
} }
if featuregate.DefaultFeatureGate.ExternalTrafficLocalOnly() && shouldCheckOrAssignHealthCheckNodePort(service) {
var healthCheckNodePort int
var err error
if l, ok := service.Annotations[apiservice.AnnotationHealthCheckNodePort]; ok {
healthCheckNodePort, err = strconv.Atoi(l)
if err != nil || healthCheckNodePort <= 0 {
return nil, errors.NewInternalError(fmt.Errorf("Failed to parse annotation %v: %v", apiservice.AnnotationHealthCheckNodePort, err))
}
}
if healthCheckNodePort > 0 {
// If the request has a health check nodePort in mind, attempt to reserve it
err := nodePortOp.Allocate(int(healthCheckNodePort))
if err != nil {
return nil, errors.NewInternalError(fmt.Errorf("Failed to allocate requested HealthCheck nodePort %v: %v", healthCheckNodePort, err))
}
} else {
// If the request has no health check nodePort specified, allocate any
healthCheckNodePort, err = nodePortOp.AllocateNext()
if err != nil {
// TODO: what error should be returned here? It's not a
// field-level validation failure (the field is valid), and it's
// not really an internal error.
return nil, errors.NewInternalError(fmt.Errorf("failed to allocate a nodePort: %v", err))
}
// Insert the newly allocated health check port as an annotation (plan of record for Alpha)
service.Annotations[apiservice.AnnotationHealthCheckNodePort] = fmt.Sprintf("%d", healthCheckNodePort)
}
}
out, err := rs.registry.CreateService(ctx, service) out, err := rs.registry.CreateService(ctx, service)
if err != nil { if err != nil {
err = rest.CheckGeneratedNameError(Strategy, err, service) err = rest.CheckGeneratedNameError(Strategy, err, service)
...@@ -398,3 +429,12 @@ func shouldAssignNodePorts(service *api.Service) bool { ...@@ -398,3 +429,12 @@ func shouldAssignNodePorts(service *api.Service) bool {
return false return false
} }
} }
func shouldCheckOrAssignHealthCheckNodePort(service *api.Service) bool {
if service.Spec.Type == api.ServiceTypeLoadBalancer {
// True if Service-type == LoadBalancer AND annotation AnnotationExternalTraffic present
return (featuregate.DefaultFeatureGate.ExternalTrafficLocalOnly() && apiservice.NeedsHealthCheck(service))
}
glog.V(4).Infof("Service type: %v does not need health check node port", service.Spec.Type)
return false
}
...@@ -24,14 +24,19 @@ import ( ...@@ -24,14 +24,19 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/rest" "k8s.io/kubernetes/pkg/api/rest"
"k8s.io/kubernetes/pkg/api/service"
"k8s.io/kubernetes/pkg/registry/registrytest" "k8s.io/kubernetes/pkg/registry/registrytest"
"k8s.io/kubernetes/pkg/registry/service/ipallocator" "k8s.io/kubernetes/pkg/registry/service/ipallocator"
"k8s.io/kubernetes/pkg/registry/service/portallocator" "k8s.io/kubernetes/pkg/registry/service/portallocator"
utilnet "k8s.io/kubernetes/pkg/util/net" featuregate "k8s.io/kubernetes/pkg/util/config"
"k8s.io/kubernetes/pkg/util/intstr" "k8s.io/kubernetes/pkg/util/intstr"
utilnet "k8s.io/kubernetes/pkg/util/net"
) )
func init() {
featuregate.DefaultFeatureGate.Set("AllowExtTrafficLocalEndpoints=true")
}
// TODO(wojtek-t): Cleanup this file. // TODO(wojtek-t): Cleanup this file.
// It is now testing mostly the same things as other resources but // It is now testing mostly the same things as other resources but
// in a completely different way. We should unify it. // in a completely different way. We should unify it.
...@@ -821,3 +826,179 @@ func TestUpdateServiceWithConflictingNamespace(t *testing.T) { ...@@ -821,3 +826,179 @@ func TestUpdateServiceWithConflictingNamespace(t *testing.T) {
t.Errorf("Expected 'Service.Namespace does not match the provided context' error, got '%s'", err.Error()) t.Errorf("Expected 'Service.Namespace does not match the provided context' error, got '%s'", err.Error())
} }
} }
// Validate allocation of a nodePort when the externalTraffic=OnlyLocal annotation is set
// and type is LoadBalancer
func TestServiceRegistryExternalTrafficAnnotationHealthCheckNodePortAllocation(t *testing.T) {
ctx := api.NewDefaultContext()
storage, _ := NewTestREST(t, nil)
svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "external-lb-esipp",
Annotations: map[string]string{
service.AnnotationExternalTraffic: service.AnnotationValueExternalTrafficLocal,
},
},
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
SessionAffinity: api.ServiceAffinityNone,
Type: api.ServiceTypeLoadBalancer,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(6502),
}},
},
}
created_svc, err := storage.Create(ctx, svc)
if created_svc == nil || err != nil {
t.Errorf("Unexpected failure creating service %v", err)
}
created_service := created_svc.(*api.Service)
if !service.NeedsHealthCheck(created_service) {
t.Errorf("Unexpected missing annotation %s", service.AnnotationExternalTraffic)
}
port := service.GetServiceHealthCheckNodePort(created_service)
if port == 0 {
t.Errorf("Failed to allocate and create the health check node port annotation %s", service.AnnotationHealthCheckNodePort)
}
}
// Validate using the user specified nodePort when the externalTraffic=OnlyLocal annotation is set
// and type is LoadBalancer
func TestServiceRegistryExternalTrafficAnnotationHealthCheckNodePortUserAllocation(t *testing.T) {
ctx := api.NewDefaultContext()
storage, _ := NewTestREST(t, nil)
svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "external-lb-esipp",
Annotations: map[string]string{
service.AnnotationExternalTraffic: service.AnnotationValueExternalTrafficLocal,
service.AnnotationHealthCheckNodePort: "30200",
},
},
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
SessionAffinity: api.ServiceAffinityNone,
Type: api.ServiceTypeLoadBalancer,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(6502),
}},
},
}
created_svc, err := storage.Create(ctx, svc)
if created_svc == nil || err != nil {
t.Errorf("Unexpected failure creating service %v", err)
}
created_service := created_svc.(*api.Service)
if !service.NeedsHealthCheck(created_service) {
t.Errorf("Unexpected missing annotation %s", service.AnnotationExternalTraffic)
}
port := service.GetServiceHealthCheckNodePort(created_service)
if port == 0 {
t.Errorf("Failed to allocate and create the health check node port annotation %s", service.AnnotationHealthCheckNodePort)
}
if port != 30200 {
t.Errorf("Failed to allocate requested nodePort expected 30200, got %d", port)
}
}
// Validate that the service creation fails when the requested port number is -1
func TestServiceRegistryExternalTrafficAnnotationNegative(t *testing.T) {
ctx := api.NewDefaultContext()
storage, _ := NewTestREST(t, nil)
svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "external-lb-esipp",
Annotations: map[string]string{
service.AnnotationExternalTraffic: service.AnnotationValueExternalTrafficLocal,
service.AnnotationHealthCheckNodePort: "-1",
},
},
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
SessionAffinity: api.ServiceAffinityNone,
Type: api.ServiceTypeLoadBalancer,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(6502),
}},
},
}
created_svc, err := storage.Create(ctx, svc)
if created_svc == nil || err != nil {
return
}
t.Errorf("Unexpected creation of service with invalid healthCheckNodePort specified")
}
// Validate that the health check nodePort is not allocated when the externalTraffic annotation is !"OnlyLocal"
func TestServiceRegistryExternalTrafficAnnotationGlobal(t *testing.T) {
ctx := api.NewDefaultContext()
storage, _ := NewTestREST(t, nil)
svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "external-lb-esipp",
Annotations: map[string]string{
service.AnnotationExternalTraffic: service.AnnotationValueExternalTrafficGlobal,
},
},
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
SessionAffinity: api.ServiceAffinityNone,
Type: api.ServiceTypeLoadBalancer,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(6502),
}},
},
}
created_svc, err := storage.Create(ctx, svc)
if created_svc == nil || err != nil {
t.Errorf("Unexpected failure creating service %v", err)
}
created_service := created_svc.(*api.Service)
// Make sure the service does not have the annotation
if service.NeedsHealthCheck(created_service) {
t.Errorf("Unexpected value for annotation %s", service.AnnotationExternalTraffic)
}
// Make sure the service does not have the health check node port allocated
port := service.GetServiceHealthCheckNodePort(created_service)
if port != 0 {
t.Errorf("Unexpected allocation of health check node port annotation %s", service.AnnotationHealthCheckNodePort)
}
}
// Validate that the health check nodePort is not allocated when service type is ClusterIP
func TestServiceRegistryExternalTrafficAnnotationClusterIP(t *testing.T) {
ctx := api.NewDefaultContext()
storage, _ := NewTestREST(t, nil)
svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "external-lb-esipp",
Annotations: map[string]string{
service.AnnotationExternalTraffic: service.AnnotationValueExternalTrafficGlobal,
},
},
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
SessionAffinity: api.ServiceAffinityNone,
Type: api.ServiceTypeClusterIP,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
TargetPort: intstr.FromInt(6502),
}},
},
}
created_svc, err := storage.Create(ctx, svc)
if created_svc == nil || err != nil {
t.Errorf("Unexpected failure creating service %v", err)
}
created_service := created_svc.(*api.Service)
// Make sure that ClusterIP services do not have the health check node port allocated
port := service.GetServiceHealthCheckNodePort(created_service)
if port != 0 {
t.Errorf("Unexpected allocation of health check node port annotation %s", service.AnnotationHealthCheckNodePort)
}
}
...@@ -38,14 +38,16 @@ const ( ...@@ -38,14 +38,16 @@ const (
// specification of gates. Examples: // specification of gates. Examples:
// AllAlpha=false,NewFeature=true will result in newFeature=true // AllAlpha=false,NewFeature=true will result in newFeature=true
// AllAlpha=true,NewFeature=false will result in newFeature=false // AllAlpha=true,NewFeature=false will result in newFeature=false
allAlphaGate = "AllAlpha" allAlphaGate = "AllAlpha"
externalTrafficLocalOnly = "AllowExtTrafficLocalEndpoints"
) )
var ( var (
// Default values for recorded features. Every new feature gate should be // Default values for recorded features. Every new feature gate should be
// represented here. // represented here.
knownFeatures = map[string]featureSpec{ knownFeatures = map[string]featureSpec{
allAlphaGate: {false, alpha}, allAlphaGate: {false, alpha},
externalTrafficLocalOnly: {false, alpha},
} }
// Special handling for a few gates. // Special handling for a few gates.
...@@ -85,6 +87,10 @@ type FeatureGate interface { ...@@ -85,6 +87,10 @@ type FeatureGate interface {
// // alpha: v1.4 // // alpha: v1.4
// MyFeature() bool // MyFeature() bool
// owner: @girishkalele
// alpha: v1.4
ExternalTrafficLocalOnly() bool
// TODO: Define accessors for each non-API alpha feature. // TODO: Define accessors for each non-API alpha feature.
} }
...@@ -154,6 +160,11 @@ func (f *featureGate) Type() string { ...@@ -154,6 +160,11 @@ func (f *featureGate) Type() string {
return "mapStringBool" return "mapStringBool"
} }
// ExternalTrafficLocalOnly returns value for AllowExtTrafficLocalEndpoints
func (f *featureGate) ExternalTrafficLocalOnly() bool {
return f.lookup(externalTrafficLocalOnly)
}
func (f *featureGate) lookup(key string) bool { func (f *featureGate) lookup(key string) bool {
defaultValue := f.known[key].enabled defaultValue := f.known[key].enabled
if f.enabled != nil { if f.enabled != nil {
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.PHONY: all netexec image push clean .PHONY: all netexec image push clean
TAG = 1.5 TAG = 1.6
PREFIX = gcr.io/google_containers PREFIX = gcr.io/google_containers
......
...@@ -80,6 +80,7 @@ func main() { ...@@ -80,6 +80,7 @@ func main() {
func startHTTPServer(httpPort int) { func startHTTPServer(httpPort int) {
http.HandleFunc("/", rootHandler) http.HandleFunc("/", rootHandler)
http.HandleFunc("/clientip", clientIpHandler)
http.HandleFunc("/echo", echoHandler) http.HandleFunc("/echo", echoHandler)
http.HandleFunc("/exit", exitHandler) http.HandleFunc("/exit", exitHandler)
http.HandleFunc("/hostname", hostnameHandler) http.HandleFunc("/hostname", hostnameHandler)
...@@ -102,6 +103,11 @@ func echoHandler(w http.ResponseWriter, r *http.Request) { ...@@ -102,6 +103,11 @@ func echoHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", r.FormValue("msg")) fmt.Fprintf(w, "%s", r.FormValue("msg"))
} }
func clientIpHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("GET /clientip")
fmt.Fprintf(w, r.RemoteAddr)
}
func exitHandler(w http.ResponseWriter, r *http.Request) { func exitHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("GET /exit?code=%s", r.FormValue("code")) log.Printf("GET /exit?code=%s", r.FormValue("code"))
code, err := strconv.Atoi(r.FormValue("code")) code, err := strconv.Atoi(r.FormValue("code"))
...@@ -345,7 +351,7 @@ func hostNameHandler(w http.ResponseWriter, r *http.Request) { ...@@ -345,7 +351,7 @@ func hostNameHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, getHostName()) fmt.Fprintf(w, getHostName())
} }
// udp server only supports the hostName command. // udp server supports the hostName, echo and clientIP commands.
func startUDPServer(udpPort int) { func startUDPServer(udpPort int) {
serverAddress, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", udpPort)) serverAddress, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", udpPort))
assertNoError(err) assertNoError(err)
...@@ -363,8 +369,8 @@ func startUDPServer(udpPort int) { ...@@ -363,8 +369,8 @@ func startUDPServer(udpPort int) {
for { for {
n, clientAddress, err := serverConn.ReadFromUDP(buf) n, clientAddress, err := serverConn.ReadFromUDP(buf)
assertNoError(err) assertNoError(err)
receivedText := strings.TrimSpace(string(buf[0:n])) receivedText := strings.ToLower(strings.TrimSpace(string(buf[0:n])))
if receivedText == "hostName" || receivedText == "hostname" { if receivedText == "hostname" {
log.Println("Sending udp hostName response") log.Println("Sending udp hostName response")
_, err = serverConn.WriteToUDP([]byte(getHostName()), clientAddress) _, err = serverConn.WriteToUDP([]byte(getHostName()), clientAddress)
assertNoError(err) assertNoError(err)
...@@ -377,6 +383,10 @@ func startUDPServer(udpPort int) { ...@@ -377,6 +383,10 @@ func startUDPServer(udpPort int) {
log.Printf("Echoing %v\n", resp) log.Printf("Echoing %v\n", resp)
_, err = serverConn.WriteToUDP([]byte(resp), clientAddress) _, err = serverConn.WriteToUDP([]byte(resp), clientAddress)
assertNoError(err) assertNoError(err)
} else if receivedText == "clientip" {
log.Printf("Sending back clientip to %s", clientAddress.String())
_, err = serverConn.WriteToUDP([]byte(clientAddress.String()), clientAddress)
assertNoError(err)
} else if len(receivedText) > 0 { } else if len(receivedText) > 0 {
log.Printf("Unknown udp command received: %v\n", receivedText) log.Printf("Unknown udp command received: %v\n", receivedText)
} }
......
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