WIP: Implement multi-port Services

This commit is contained in:
Tim Hockin
2015-03-13 08:16:41 -07:00
parent 9ed87612d0
commit 186818d787
70 changed files with 2118 additions and 815 deletions

View File

@@ -5722,4 +5722,4 @@
} }
} }
} }
} }

View File

@@ -6,8 +6,9 @@ metadata:
kubernetes.io/cluster-service: "true" kubernetes.io/cluster-service: "true"
name: monitoring-grafana name: monitoring-grafana
spec: spec:
targetPort: 80 ports:
port: 80 - port: 80
targetPort: 80
selector: selector:
name: influxGrafana name: influxGrafana
kubernetes.io/cluster-service: "true" kubernetes.io/cluster-service: "true"

View File

@@ -6,8 +6,9 @@ metadata:
kubernetes.io/cluster-service: "true" kubernetes.io/cluster-service: "true"
name: monitoring-heapster name: monitoring-heapster
spec: spec:
targetPort: 8082 ports:
port: 80 - port: 80
targetPort: 8082
selector: selector:
name: heapster name: heapster
kubernetes.io/cluster-service: "true" kubernetes.io/cluster-service: "true"

View File

@@ -5,8 +5,9 @@ metadata:
name: influxGrafana name: influxGrafana
name: monitoring-influxdb name: monitoring-influxdb
spec: spec:
targetPort: 8086 ports:
port: 80 - port: 80
targetPort: 8086
selector: selector:
name: influxGrafana name: influxGrafana

View File

@@ -5,8 +5,9 @@ metadata:
name: influxGrafana name: influxGrafana
name: monitoring-influxdb-ui name: monitoring-influxdb-ui
spec: spec:
targetPort: 8083 ports:
port: 80 - port: 80
targetPort: 8083
selector: selector:
name: influxGrafana name: influxGrafana

View File

@@ -465,12 +465,14 @@ func runSelfLinkTestOnNamespace(c *client.Client, namespace string) {
}, },
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 12345,
// This is here because validation requires it. // This is here because validation requires it.
Selector: map[string]string{ Selector: map[string]string{
"foo": "bar", "foo": "bar",
}, },
Protocol: "TCP", Ports: []api.ServicePort{{
Port: 12345,
Protocol: "TCP",
}},
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
@@ -527,12 +529,14 @@ func runAtomicPutTest(c *client.Client) {
}, },
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 12345,
// This is here because validation requires it. // This is here because validation requires it.
Selector: map[string]string{ Selector: map[string]string{
"foo": "bar", "foo": "bar",
}, },
Protocol: "TCP", Ports: []api.ServicePort{{
Port: 12345,
Protocol: "TCP",
}},
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
@@ -606,12 +610,14 @@ func runPatchTest(c *client.Client) {
}, },
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 12345,
// This is here because validation requires it. // This is here because validation requires it.
Selector: map[string]string{ Selector: map[string]string{
"foo": "bar", "foo": "bar",
}, },
Protocol: "TCP", Ports: []api.ServicePort{{
Port: 12345,
Protocol: "TCP",
}},
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
@@ -747,8 +753,10 @@ func runServiceTest(client *client.Client) {
Selector: map[string]string{ Selector: map[string]string{
"name": "thisisalonglabel", "name": "thisisalonglabel",
}, },
Port: 8080, Ports: []api.ServicePort{{
Protocol: "TCP", Port: 8080,
Protocol: "TCP",
}},
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
@@ -764,8 +772,10 @@ func runServiceTest(client *client.Client) {
Selector: map[string]string{ Selector: map[string]string{
"name": "thisisalonglabel", "name": "thisisalonglabel",
}, },
Port: 8080, Ports: []api.ServicePort{{
Protocol: "TCP", Port: 8080,
Protocol: "TCP",
}},
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }
@@ -784,8 +794,10 @@ func runServiceTest(client *client.Client) {
Selector: map[string]string{ Selector: map[string]string{
"name": "thisisalonglabel", "name": "thisisalonglabel",
}, },
Port: 8080, Ports: []api.ServicePort{{
Protocol: "TCP", Port: 8080,
Protocol: "TCP",
}},
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }

View File

@@ -8,11 +8,15 @@
} }
}, },
"spec": { "spec": {
"port": 8080, "ports": [
"protocol": "TCP", {
"port": 8080,
"protocol": "TCP",
"targetPort": 8080
}
],
"selector": { "selector": {
"name": "nettest" "name": "nettest"
}, }
"targetPort": 8080,
} }
} }

View File

@@ -5,7 +5,8 @@ metadata:
name: cassandra name: cassandra
name: cassandra name: cassandra
spec: spec:
targetPort: 9042 ports:
port: 9042 - port: 9042
targetPort: 9042
selector: selector:
name: cassandra name: cassandra

View File

@@ -8,9 +8,13 @@
} }
}, },
"spec":{ "spec":{
"port":3000, "ports": [
"containerPort":"http-server", {
"protocol":"TCP", "port":3000,
"targetPort":"http-server",
"protocol":"TCP"
}
],
"selector":{ "selector":{
"name":"guestbook" "name":"guestbook"
} }

View File

@@ -9,9 +9,13 @@
} }
}, },
"spec":{ "spec":{
"port":6379, "ports": [
"containerPort":"redis-server", {
"protocol":"TCP", "port":6379,
"targetPort":"redis-server",
"protocol":"TCP"
}
],
"selector":{ "selector":{
"name":"redis", "name":"redis",
"role":"master" "role":"master"

View File

@@ -9,9 +9,13 @@
} }
}, },
"spec":{ "spec":{
"port":6379, "ports": [
"containerPort":"redis-server", {
"protocol":"TCP", "port":6379,
"targetPort":"redis-server",
"protocol":"TCP"
}
],
"selector":{ "selector":{
"name":"redis", "name":"redis",
"role":"slave" "role":"slave"

View File

@@ -8,9 +8,13 @@
} }
}, },
"spec":{ "spec":{
"port":80, "ports": [
"containerPort":80, {
"protocol":"TCP", "port":80,
"targetPort":80,
"protocol":"TCP"
}
],
"selector":{ "selector":{
"name":"frontend" "name":"frontend"
} }

View File

@@ -8,9 +8,13 @@
} }
}, },
"spec":{ "spec":{
"port":6379, "ports": [
"containerPort":6379, {
"protocol":"TCP", "port":6379,
"targetPort":6379,
"protocol":"TCP"
}
],
"selector":{ "selector":{
"name":"redis-master" "name":"redis-master"
} }

View File

@@ -8,9 +8,13 @@
} }
}, },
"spec":{ "spec":{
"port":6379, "ports": [
"containerPort":6379, {
"protocol":"TCP", "port":6379,
"targetPort":6379,
"protocol":"TCP"
}
],
"selector":{ "selector":{
"name":"redis-slave" "name":"redis-slave"
} }

View File

@@ -5,7 +5,8 @@ metadata:
name: hazelcast name: hazelcast
name: hazelcast name: hazelcast
spec: spec:
targetPort: 5701 ports:
port: 5701 - port: 5701
targetPort: 5701
selector: selector:
name: hazelcast name: hazelcast

View File

@@ -5,8 +5,9 @@ metadata:
name: mysql name: mysql
name: mysql name: mysql
spec: spec:
targetPort: 3306 ports:
port: 3306 - port: 3306
targetPort: 3306
selector: selector:
name: mysql name: mysql

View File

@@ -5,8 +5,9 @@ metadata:
name: wpfrontend name: wpfrontend
name: wpfrontend name: wpfrontend
spec: spec:
targetPort: 80 ports:
port: 80 - port: 80
targetPort: 80
selector: selector:
name: wpfrontend name: wpfrontend

View File

@@ -6,7 +6,8 @@ metadata:
role: service role: service
name: redis-sentinel name: redis-sentinel
spec: spec:
targetPort: 26379 ports:
port: 26379 - port: 26379
targetPort: 26379
selector: selector:
redis-sentinel: "true" redis-sentinel: "true"

View File

@@ -6,8 +6,9 @@ metadata:
name: rethinkdb-admin name: rethinkdb-admin
namespace: rethinkdb namespace: rethinkdb
spec: spec:
targetPort: 8080 ports:
port: 8080 - port: 8080
targetPort: 8080
selector: selector:
db: rethinkdb db: rethinkdb
role: admin role: admin

View File

@@ -6,7 +6,8 @@ metadata:
name: rethinkdb-driver name: rethinkdb-driver
namespace: rethinkdb namespace: rethinkdb
spec: spec:
targetPort: 28015 ports:
port: 28015 - port: 28015
targetPort: 28015
selector: selector:
db: rethinkdb db: rethinkdb

View File

@@ -3,16 +3,14 @@ kind: Service
metadata: metadata:
name: nginx-example name: nginx-example
spec: spec:
# the container on each pod to connect to, can be a name ports:
# (e.g. 'www') or a number (e.g. 80) - port: 8000 # the port that this service should serve on
targetPort: 80 # the container on each pod to connect to, can be a name
# the port that this service should serve on # (e.g. 'www') or a number (e.g. 80)
port: 8000 targetPort: 80
protocol: TCP protocol: TCP
# just like the selector in the replication controller, # just like the selector in the replication controller,
# but this time it identifies the set of pods to load balance # but this time it identifies the set of pods to load balance
# traffic to. # traffic to.
selector: selector:
name: nginx name: nginx

View File

@@ -97,12 +97,27 @@ func (set addressSet) Insert(addr *api.EndpointAddress) {
func hashAddresses(addrs addressSet) string { func hashAddresses(addrs addressSet) string {
// Flatten the list of addresses into a string so it can be used as a // Flatten the list of addresses into a string so it can be used as a
// map key. // map key. Unfortunately, DeepHashObject is implemented in terms of
// spew, and spew does not handle non-primitive mapo keys well. So
// first we collapse it into a slice, sort the slice, then hash that.
slice := []*api.EndpointAddress{}
for k := range addrs {
slice = append(slice, k)
}
sort.Sort(addrPtrsByIP(slice))
hasher := md5.New() hasher := md5.New()
util.DeepHashObject(hasher, addrs) util.DeepHashObject(hasher, slice)
return hex.EncodeToString(hasher.Sum(nil)[0:]) return hex.EncodeToString(hasher.Sum(nil)[0:])
} }
type addrPtrsByIP []*api.EndpointAddress
func (sl addrPtrsByIP) Len() int { return len(sl) }
func (sl addrPtrsByIP) Swap(i, j int) { sl[i], sl[j] = sl[j], sl[i] }
func (sl addrPtrsByIP) Less(i, j int) bool {
return bytes.Compare([]byte(sl[i].IP), []byte(sl[j].IP)) < 0
}
// SortSubsets sorts an array of EndpointSubset objects in place. For ease of // SortSubsets sorts an array of EndpointSubset objects in place. For ease of
// use it returns the input slice. // use it returns the input slice.
func SortSubsets(subsets []api.EndpointSubset) []api.EndpointSubset { func SortSubsets(subsets []api.EndpointSubset) []api.EndpointSubset {

View File

@@ -216,11 +216,18 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
}, },
func(ss *api.ServiceSpec, c fuzz.Continue) { func(ss *api.ServiceSpec, c fuzz.Continue) {
c.FuzzNoCustom(ss) // fuzz self without calling this function again c.FuzzNoCustom(ss) // fuzz self without calling this function again
switch ss.TargetPort.Kind { if len(ss.Ports) == 0 {
case util.IntstrInt: // There must be at least 1 port.
ss.TargetPort.IntVal = 1 + ss.TargetPort.IntVal%65535 // non-zero ss.Ports = append(ss.Ports, api.ServicePort{})
case util.IntstrString: c.Fuzz(&ss.Ports[0])
ss.TargetPort.StrVal = "x" + ss.TargetPort.StrVal // non-empty }
for i := range ss.Ports {
switch ss.Ports[i].TargetPort.Kind {
case util.IntstrInt:
ss.Ports[i].TargetPort.IntVal = 1 + ss.Ports[i].TargetPort.IntVal%65535 // non-zero
case util.IntstrString:
ss.Ports[i].TargetPort.StrVal = "x" + ss.Ports[i].TargetPort.StrVal // non-empty
}
} }
}, },
) )

View File

@@ -863,12 +863,8 @@ type ServiceStatus struct{}
// ServiceSpec describes the attributes that a user creates on a service // ServiceSpec describes the attributes that a user creates on a service
type ServiceSpec struct { type ServiceSpec struct {
// Port is the TCP or UDP port that will be made available to each pod for connecting to the pods // Required: The list of ports that are exposed by this service.
// proxied by this service. Ports []ServicePort `json:"ports"`
Port int `json:"port"`
// Required: Supports "TCP" and "UDP".
Protocol Protocol `json:"protocol,omitempty"`
// This service will route traffic to pods having labels matching this selector. If empty or not present, // This service will route traffic to pods having labels matching this selector. If empty or not present,
// the service is assumed to have endpoints set by an external process and Kubernetes will not modify // the service is assumed to have endpoints set by an external process and Kubernetes will not modify
@@ -891,17 +887,32 @@ type ServiceSpec struct {
// For hostnames, the user will use a CNAME record (instead of using an A record with the IP) // For hostnames, the user will use a CNAME record (instead of using an A record with the IP)
PublicIPs []string `json:"publicIPs,omitempty"` PublicIPs []string `json:"publicIPs,omitempty"`
// TargetPort is the name or number of the port on the container to direct traffic to.
// This is useful if the containers the service points to have multiple open ports.
// Optional: If unspecified, the first port on the container will be used.
// As of v1beta3 this field will become required in the internal API,
// and the versioned APIs must provide a default value.
TargetPort util.IntOrString `json:"targetPort,omitempty"`
// Required: Supports "ClientIP" and "None". Used to maintain session affinity. // Required: Supports "ClientIP" and "None". Used to maintain session affinity.
SessionAffinity AffinityType `json:"sessionAffinity,omitempty"` SessionAffinity AffinityType `json:"sessionAffinity,omitempty"`
} }
type ServicePort struct {
// Optional if only one ServicePort is defined on this service: The
// name of this port within the service. This must be a DNS_LABEL.
// All ports within a ServiceSpec must have unique names. This maps to
// the 'Name' field in EndpointPort objects.
Name string `json:"name"`
// The IP protocol for this port. Supports "TCP" and "UDP".
Protocol Protocol `json:"protocol"`
// The port that will be exposed on the service.
Port int `json:"port"`
// Optional: The target port on pods selected by this service. If this
// is a string, it will be looked up as a named port in the target
// Pod's container ports. If this is not specified, the first port on
// the destination pod will be used. This behavior is deprecated. As
// of v1beta3 the default value is the sames as the Port field (an
// identity map).
TargetPort util.IntOrString `json:"targetPort"`
}
// Service is a named abstraction of software service (for example, mysql) consisting of local port // Service is a named abstraction of software service (for example, mysql) consisting of local port
// (for example 3306) that the proxy listens on, and the selector that determines which pods // (for example 3306) that the proxy listens on, and the selector that determines which pods
// will answer requests sent through the proxy. // will answer requests sent through the proxy.

View File

@@ -703,14 +703,28 @@ func init() {
return err return err
} }
out.Port = in.Spec.Port // Produce legacy fields.
out.Protocol = Protocol(in.Spec.Protocol) if len(in.Spec.Ports) > 0 {
out.PortName = in.Spec.Ports[0].Name
out.Port = in.Spec.Ports[0].Port
out.Protocol = Protocol(in.Spec.Ports[0].Protocol)
out.ContainerPort = in.Spec.Ports[0].TargetPort
}
// Copy modern fields.
for i := range in.Spec.Ports {
out.Ports = append(out.Ports, ServicePort{
Name: in.Spec.Ports[i].Name,
Port: in.Spec.Ports[i].Port,
Protocol: Protocol(in.Spec.Ports[i].Protocol),
ContainerPort: in.Spec.Ports[i].TargetPort,
})
}
if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil { if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil {
return err return err
} }
out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer
out.PublicIPs = in.Spec.PublicIPs out.PublicIPs = in.Spec.PublicIPs
out.ContainerPort = in.Spec.TargetPort
out.PortalIP = in.Spec.PortalIP out.PortalIP = in.Spec.PortalIP
if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil { if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil {
return err return err
@@ -729,14 +743,31 @@ func init() {
return err return err
} }
out.Spec.Port = in.Port if len(in.Ports) == 0 && in.Port != 0 {
out.Spec.Protocol = newer.Protocol(in.Protocol) // Use legacy fields to produce modern fields.
out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{
Name: in.PortName,
Port: in.Port,
Protocol: newer.Protocol(in.Protocol),
TargetPort: in.ContainerPort,
})
} else {
// Use modern fields, ignore legacy.
for i := range in.Ports {
out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{
Name: in.Ports[i].Name,
Port: in.Ports[i].Port,
Protocol: newer.Protocol(in.Ports[i].Protocol),
TargetPort: in.Ports[i].ContainerPort,
})
}
}
if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil { if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil {
return err return err
} }
out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer
out.Spec.PublicIPs = in.PublicIPs out.Spec.PublicIPs = in.PublicIPs
out.Spec.TargetPort = in.ContainerPort
out.Spec.PortalIP = in.PortalIP out.Spec.PortalIP = in.PortalIP
if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil { if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil {
return err return err

View File

@@ -284,6 +284,191 @@ func TestServiceEmptySelector(t *testing.T) {
} }
} }
func TestServicePorts(t *testing.T) {
testCases := []struct {
given current.Service
expected newer.Service
roundtrip current.Service
}{
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "legacy-with-defaults",
},
Port: 111,
Protocol: current.ProtocolTCP,
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Port: 111,
Protocol: newer.ProtocolTCP,
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Port: 111,
Protocol: current.ProtocolTCP,
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "legacy-full",
},
PortName: "p",
Port: 111,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromString("p"),
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "p",
Port: 111,
Protocol: newer.ProtocolTCP,
TargetPort: util.NewIntOrStringFromString("p"),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromString("p"),
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "both",
},
PortName: "p",
Port: 111,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromString("p"),
Ports: []current.ServicePort{{
Name: "q",
Port: 222,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "q",
Port: 222,
Protocol: newer.ProtocolUDP,
TargetPort: util.NewIntOrStringFromInt(93),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "q",
Port: 222,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "one",
},
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "p",
Port: 111,
Protocol: newer.ProtocolUDP,
TargetPort: util.NewIntOrStringFromInt(93),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "two",
},
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}, {
Name: "q",
Port: 222,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromInt(76),
}},
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "p",
Port: 111,
Protocol: newer.ProtocolUDP,
TargetPort: util.NewIntOrStringFromInt(93),
}, {
Name: "q",
Port: 222,
Protocol: newer.ProtocolTCP,
TargetPort: util.NewIntOrStringFromInt(76),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}, {
Name: "q",
Port: 222,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromInt(76),
}},
},
},
}
for i, tc := range testCases {
// Convert versioned -> internal.
got := newer.Service{}
if err := Convert(&tc.given, &got); err != nil {
t.Errorf("[Case: %d] Unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(got.Spec.Ports, tc.expected.Spec.Ports) {
t.Errorf("[Case: %d] Expected %v, got %v", i, tc.expected.Spec.Ports, got.Spec.Ports)
}
// Convert internal -> versioned.
got2 := current.Service{}
if err := Convert(&got, &got2); err != nil {
t.Errorf("[Case: %d] Unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(got2.Ports, tc.roundtrip.Ports) {
t.Errorf("[Case: %d] Expected %v, got %v", i, tc.roundtrip.Ports, got2.Ports)
}
}
}
func TestPullPolicyConversion(t *testing.T) { func TestPullPolicyConversion(t *testing.T) {
table := []struct { table := []struct {
versioned current.PullPolicy versioned current.PullPolicy
@@ -527,7 +712,7 @@ func TestEndpointsConversion(t *testing.T) {
continue continue
} }
if got2.Protocol != tc.given.Protocol || !newer.Semantic.DeepEqual(got2.Endpoints, tc.given.Endpoints) { if got2.Protocol != tc.given.Protocol || !newer.Semantic.DeepEqual(got2.Endpoints, tc.given.Endpoints) {
t.Errorf("[Case: %d] Expected %#v, got %#v", i, tc.given.Endpoints, got2.Endpoints) t.Errorf("[Case: %d] Expected %s %#v, got %s %#v", i, tc.given.Protocol, tc.given.Endpoints, got2.Protocol, got2.Endpoints)
} }
} }
} }

View File

@@ -68,6 +68,14 @@ func init() {
obj.SessionAffinity = AffinityTypeNone obj.SessionAffinity = AffinityTypeNone
} }
}, },
func(obj *ServicePort) {
if obj.Protocol == "" {
obj.Protocol = ProtocolTCP
}
if obj.ContainerPort == util.NewIntOrStringFromInt(0) || obj.ContainerPort == util.NewIntOrStringFromString("") {
obj.ContainerPort = util.NewIntOrStringFromInt(obj.Port)
}
},
func(obj *PodSpec) { func(obj *PodSpec) {
if obj.DNSPolicy == "" { if obj.DNSPolicy == "" {
obj.DNSPolicy = DNSClusterFirst obj.DNSPolicy = DNSClusterFirst

View File

@@ -22,6 +22,7 @@ import (
current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
) )
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
@@ -148,3 +149,35 @@ func TestSetDefaultContainerManifestHostNetwork(t *testing.T) {
t.Errorf("Expected container port to be defaulted, was made %d instead of %d", hostPortNum, portNum) t.Errorf("Expected container port to be defaulted, was made %d instead of %d", hostPortNum, portNum)
} }
} }
func TestSetDefaultServicePort(t *testing.T) {
// Unchanged if set.
in := &current.Service{Ports: []current.ServicePort{{Protocol: "UDP", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(118)}}}
out := roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Ports[0].Protocol != current.ProtocolUDP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Ports[0].Protocol)
}
if out.Ports[0].ContainerPort != in.Ports[0].ContainerPort {
t.Errorf("Expected port %d, got %d", in.Ports[0].ContainerPort, out.Ports[0].ContainerPort)
}
// Defaulted.
in = &current.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(0)}}}
out = roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Ports[0].Protocol != current.ProtocolTCP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol)
}
if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) {
t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort)
}
// Defaulted.
in = &current.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromString("")}}}
out = roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Ports[0].Protocol != current.ProtocolTCP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol)
}
if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) {
t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort)
}
}

View File

@@ -718,14 +718,21 @@ type Service struct {
// Required. // Required.
Port int `json:"port" description:"port exposed by the service"` Port int `json:"port" description:"port exposed by the service"`
// Optional: The name of the first port.
PortName string `json:"portName,omitempty" description:"the name of the first port; optional"`
// Optional: Defaults to "TCP". // Optional: Defaults to "TCP".
Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"`
// ContainerPort is the name or number of the port on the container to direct traffic to.
// This is useful if the containers the service points to have multiple open ports.
// Optional: If unspecified, the first port on the container will be used.
ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"`
// This service's labels. // This service's labels.
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize services"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize services"`
// This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected.
Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"`
// An external load balancer should be set up via the cloud-provider // An external load balancer should be set up via the cloud-provider
CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"`
@@ -733,11 +740,6 @@ type Service struct {
// users to handle external traffic that arrives at a node. // users to handle external traffic that arrives at a node.
PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"`
// ContainerPort is the name or number of the port on the container to direct traffic to.
// This is useful if the containers the service points to have multiple open ports.
// Optional: If unspecified, the first port on the container will be used.
ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"`
// PortalIP is usually assigned by the master. If specified by the user // PortalIP is usually assigned by the master. If specified by the user
// we will try to respect it or else fail the request. This field can // we will try to respect it or else fail the request. This field can
// not be changed by updates. // not be changed by updates.
@@ -750,6 +752,35 @@ type Service struct {
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity. // Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
// Optional: Ports to expose on the service. If this field is
// specified, the legacy fields (Port, PortName, Protocol, and
// ContainerPort) will be overwritten by the first member of this
// array. If this field is not specified, it will be populated from
// the legacy fields.
Ports []ServicePort `json:"ports" description:"ports to be exposed on the service"`
}
type ServicePort struct {
// Required: The name of this port within the service. This must be a
// DNS_LABEL. All ports within a ServiceSpec must have unique names.
// This maps to the 'Name' field in EndpointPort objects.
Name string `json:"name" description:"the name of this port; optional if only one port is defined"`
// Optional: The IP protocol for this port. Supports "TCP" and "UDP",
// default is TCP.
Protocol Protocol `json:"protocol" description:"the protocol used by this port; must be UDP or TCP; TCP if unspecified"`
// Required: The port that will be exposed.
Port int `json:"port" description:"the port number that is exposed"`
// Optional: The port number on the target pod to direct traffic to.
// This is useful if the containers the service points to have multiple
// open ports. If this is a string, it will be looked up as a named
// port in the target Pod's container ports. If unspecified, the value
// of Port is used (an identity map) - note this is a different default
// than Service.ContainerPort.
ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"`
} }
// EndpointObjectReference is a reference to an object exposing the endpoint // EndpointObjectReference is a reference to an object exposing the endpoint

View File

@@ -635,14 +635,28 @@ func init() {
return err return err
} }
out.Port = in.Spec.Port // Produce legacy fields.
out.Protocol = Protocol(in.Spec.Protocol) if len(in.Spec.Ports) > 0 {
out.PortName = in.Spec.Ports[0].Name
out.Port = in.Spec.Ports[0].Port
out.Protocol = Protocol(in.Spec.Ports[0].Protocol)
out.ContainerPort = in.Spec.Ports[0].TargetPort
}
// Copy modern fields.
for i := range in.Spec.Ports {
out.Ports = append(out.Ports, ServicePort{
Name: in.Spec.Ports[i].Name,
Port: in.Spec.Ports[i].Port,
Protocol: Protocol(in.Spec.Ports[i].Protocol),
ContainerPort: in.Spec.Ports[i].TargetPort,
})
}
if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil { if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil {
return err return err
} }
out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer
out.PublicIPs = in.Spec.PublicIPs out.PublicIPs = in.Spec.PublicIPs
out.ContainerPort = in.Spec.TargetPort
out.PortalIP = in.Spec.PortalIP out.PortalIP = in.Spec.PortalIP
if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil { if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil {
return err return err
@@ -661,14 +675,31 @@ func init() {
return err return err
} }
out.Spec.Port = in.Port if len(in.Ports) == 0 && in.Port != 0 {
out.Spec.Protocol = newer.Protocol(in.Protocol) // Use legacy fields to produce modern fields.
out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{
Name: in.PortName,
Port: in.Port,
Protocol: newer.Protocol(in.Protocol),
TargetPort: in.ContainerPort,
})
} else {
// Use modern fields, ignore legacy.
for i := range in.Ports {
out.Spec.Ports = append(out.Spec.Ports, newer.ServicePort{
Name: in.Ports[i].Name,
Port: in.Ports[i].Port,
Protocol: newer.Protocol(in.Ports[i].Protocol),
TargetPort: in.Ports[i].ContainerPort,
})
}
}
if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil { if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil {
return err return err
} }
out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer
out.Spec.PublicIPs = in.PublicIPs out.Spec.PublicIPs = in.PublicIPs
out.Spec.TargetPort = in.ContainerPort
out.Spec.PortalIP = in.PortalIP out.Spec.PortalIP = in.PortalIP
if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil { if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil {
return err return err

View File

@@ -18,6 +18,7 @@ package v1beta2_test
import ( import (
"encoding/json" "encoding/json"
"reflect"
"testing" "testing"
newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
@@ -58,6 +59,191 @@ func TestServiceEmptySelector(t *testing.T) {
} }
} }
func TestServicePorts(t *testing.T) {
testCases := []struct {
given current.Service
expected newer.Service
roundtrip current.Service
}{
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "legacy-with-defaults",
},
Port: 111,
Protocol: current.ProtocolTCP,
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Port: 111,
Protocol: newer.ProtocolTCP,
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Port: 111,
Protocol: current.ProtocolTCP,
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "legacy-full",
},
PortName: "p",
Port: 111,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromString("p"),
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "p",
Port: 111,
Protocol: newer.ProtocolTCP,
TargetPort: util.NewIntOrStringFromString("p"),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromString("p"),
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "both",
},
PortName: "p",
Port: 111,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromString("p"),
Ports: []current.ServicePort{{
Name: "q",
Port: 222,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "q",
Port: 222,
Protocol: newer.ProtocolUDP,
TargetPort: util.NewIntOrStringFromInt(93),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "q",
Port: 222,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "one",
},
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "p",
Port: 111,
Protocol: newer.ProtocolUDP,
TargetPort: util.NewIntOrStringFromInt(93),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}},
},
},
{
given: current.Service{
TypeMeta: current.TypeMeta{
ID: "two",
},
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}, {
Name: "q",
Port: 222,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromInt(76),
}},
},
expected: newer.Service{
Spec: newer.ServiceSpec{Ports: []newer.ServicePort{{
Name: "p",
Port: 111,
Protocol: newer.ProtocolUDP,
TargetPort: util.NewIntOrStringFromInt(93),
}, {
Name: "q",
Port: 222,
Protocol: newer.ProtocolTCP,
TargetPort: util.NewIntOrStringFromInt(76),
}}},
},
roundtrip: current.Service{
Ports: []current.ServicePort{{
Name: "p",
Port: 111,
Protocol: current.ProtocolUDP,
ContainerPort: util.NewIntOrStringFromInt(93),
}, {
Name: "q",
Port: 222,
Protocol: current.ProtocolTCP,
ContainerPort: util.NewIntOrStringFromInt(76),
}},
},
},
}
for i, tc := range testCases {
// Convert versioned -> internal.
got := newer.Service{}
if err := newer.Scheme.Convert(&tc.given, &got); err != nil {
t.Errorf("[Case: %d] Unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(got.Spec.Ports, tc.expected.Spec.Ports) {
t.Errorf("[Case: %d] Expected %v, got %v", i, tc.expected.Spec.Ports, got.Spec.Ports)
}
// Convert internal -> versioned.
got2 := current.Service{}
if err := newer.Scheme.Convert(&got, &got2); err != nil {
t.Errorf("[Case: %d] Unexpected error: %v", i, err)
continue
}
if !reflect.DeepEqual(got2.Ports, tc.roundtrip.Ports) {
t.Errorf("[Case: %d] Expected %v, got %v", i, tc.roundtrip.Ports, got2.Ports)
}
}
}
func TestNodeConversion(t *testing.T) { func TestNodeConversion(t *testing.T) {
version, kind, err := newer.Scheme.ObjectVersionAndKind(&current.Minion{}) version, kind, err := newer.Scheme.ObjectVersionAndKind(&current.Minion{})
if err != nil { if err != nil {

View File

@@ -69,6 +69,14 @@ func init() {
obj.SessionAffinity = AffinityTypeNone obj.SessionAffinity = AffinityTypeNone
} }
}, },
func(obj *ServicePort) {
if obj.Protocol == "" {
obj.Protocol = ProtocolTCP
}
if obj.ContainerPort == util.NewIntOrStringFromInt(0) || obj.ContainerPort == util.NewIntOrStringFromString("") {
obj.ContainerPort = util.NewIntOrStringFromInt(obj.Port)
}
},
func(obj *PodSpec) { func(obj *PodSpec) {
if obj.DNSPolicy == "" { if obj.DNSPolicy == "" {
obj.DNSPolicy = DNSClusterFirst obj.DNSPolicy = DNSClusterFirst

View File

@@ -22,6 +22,7 @@ import (
current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" current "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
) )
func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { func roundTrip(t *testing.T, obj runtime.Object) runtime.Object {
@@ -147,3 +148,35 @@ func TestSetDefaultContainerManifestHostNetwork(t *testing.T) {
t.Errorf("Expected container port to be defaulted, was made %d instead of %d", hostPortNum, portNum) t.Errorf("Expected container port to be defaulted, was made %d instead of %d", hostPortNum, portNum)
} }
} }
func TestSetDefaultServicePort(t *testing.T) {
// Unchanged if set.
in := &current.Service{Ports: []current.ServicePort{{Protocol: "UDP", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(118)}}}
out := roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Ports[0].Protocol != current.ProtocolUDP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Ports[0].Protocol)
}
if out.Ports[0].ContainerPort != in.Ports[0].ContainerPort {
t.Errorf("Expected port %d, got %d", in.Ports[0].ContainerPort, out.Ports[0].ContainerPort)
}
// Defaulted.
in = &current.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromInt(0)}}}
out = roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Ports[0].Protocol != current.ProtocolTCP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol)
}
if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) {
t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort)
}
// Defaulted.
in = &current.Service{Ports: []current.ServicePort{{Protocol: "", Port: 9376, ContainerPort: util.NewIntOrStringFromString("")}}}
out = roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Ports[0].Protocol != current.ProtocolTCP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Ports[0].Protocol)
}
if out.Ports[0].ContainerPort != util.NewIntOrStringFromInt(in.Ports[0].Port) {
t.Errorf("Expected port %d, got %v", in.Ports[0].Port, out.Ports[0].ContainerPort)
}
}

View File

@@ -719,14 +719,21 @@ type Service struct {
// Required. // Required.
Port int `json:"port" description:"port exposed by the service"` Port int `json:"port" description:"port exposed by the service"`
// Optional: The name of the first port.
PortName string `json:"portName,omitempty" description:"the name of the first port; optional"`
// Optional: Defaults to "TCP". // Optional: Defaults to "TCP".
Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"` Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"`
// ContainerPort is the name or number of the port on the container to direct traffic to.
// This is useful if the containers the service points to have multiple open ports.
// Optional: If unspecified, the first port on the container will be used.
ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"`
// This service's labels. // This service's labels.
Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize services"` Labels map[string]string `json:"labels,omitempty" description:"map of string keys and values that can be used to organize and categorize services"`
// This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected.
Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"`
// An external load balancer should be set up via the cloud-provider // An external load balancer should be set up via the cloud-provider
CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"`
@@ -734,11 +741,6 @@ type Service struct {
// users to handle external traffic that arrives at a node. // users to handle external traffic that arrives at a node.
PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"`
// ContainerPort is the name or number of the port on the container to direct traffic to.
// This is useful if the containers the service points to have multiple open ports.
// Optional: If unspecified, the first port on the container will be used.
ContainerPort util.IntOrString `json:"containerPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"`
// PortalIP is usually assigned by the master. If specified by the user // PortalIP is usually assigned by the master. If specified by the user
// we will try to respect it or else fail the request. This field can // we will try to respect it or else fail the request. This field can
// not be changed by updates. // not be changed by updates.
@@ -751,6 +753,35 @@ type Service struct {
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity. // Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
// Optional: Ports to expose on the service. If this field is
// specified, the legacy fields (Port, PortName, Protocol, and
// ContainerPort) will be overwritten by the first member of this
// array. If this field is not specified, it will be populated from
// the legacy fields.
Ports []ServicePort `json:"ports" description:"ports to be exposed on the service"`
}
type ServicePort struct {
// Required: The name of this port within the service. This must be a
// DNS_LABEL. All ports within a ServiceSpec must have unique names.
// This maps to the 'Name' field in EndpointPort objects.
Name string `json:"name" description:"the name of this port; optional if only one port is defined"`
// Optional: The IP protocol for this port. Supports "TCP" and "UDP",
// default is TCP.
Protocol Protocol `json:"protocol" description:"the protocol used by this port; must be UDP or TCP; TCP if unspecified"`
// Required: The port that will be exposed.
Port int `json:"port" description:"the port number that is exposed"`
// Optional: The port number on the target pod to direct traffic to.
// This is useful if the containers the service points to have multiple
// open ports. If this is a string, it will be looked up as a named
// port in the target Pod's container ports. If unspecified, the value
// of Port is used (an identity map) - note this is a different default
// than Service.ContainerPort.
ContainerPort util.IntOrString `json:"containerPort" description:"the port to access on the containers belonging to pods targeted by the service; defaults to the service port"`
} }
// EndpointObjectReference is a reference to an object exposing the endpoint // EndpointObjectReference is a reference to an object exposing the endpoint

View File

@@ -53,13 +53,18 @@ func init() {
} }
}, },
func(obj *Service) { func(obj *Service) {
if obj.Spec.Protocol == "" {
obj.Spec.Protocol = ProtocolTCP
}
if obj.Spec.SessionAffinity == "" { if obj.Spec.SessionAffinity == "" {
obj.Spec.SessionAffinity = AffinityTypeNone obj.Spec.SessionAffinity = AffinityTypeNone
} }
}, },
func(obj *ServicePort) {
if obj.Protocol == "" {
obj.Protocol = ProtocolTCP
}
if obj.TargetPort == util.NewIntOrStringFromInt(0) || obj.TargetPort == util.NewIntOrStringFromString("") {
obj.TargetPort = util.NewIntOrStringFromInt(obj.Port)
}
},
func(obj *PodSpec) { func(obj *PodSpec) {
if obj.DNSPolicy == "" { if obj.DNSPolicy == "" {
obj.DNSPolicy = DNSClusterFirst obj.DNSPolicy = DNSClusterFirst
@@ -97,9 +102,11 @@ func init() {
obj.Path = "/" obj.Path = "/"
} }
}, },
func(obj *ServiceSpec) { func(obj *ServicePort) {
if obj.TargetPort.Kind == util.IntstrInt && obj.TargetPort.IntVal == 0 || if obj.Protocol == "" {
obj.TargetPort.Kind == util.IntstrString && obj.TargetPort.StrVal == "" { obj.Protocol = ProtocolTCP
}
if obj.TargetPort == util.NewIntOrStringFromInt(0) || obj.TargetPort == util.NewIntOrStringFromString("") {
obj.TargetPort = util.NewIntOrStringFromInt(obj.Port) obj.TargetPort = util.NewIntOrStringFromInt(obj.Port)
} }
}, },

View File

@@ -44,9 +44,6 @@ func TestSetDefaultService(t *testing.T) {
svc := &current.Service{} svc := &current.Service{}
obj2 := roundTrip(t, runtime.Object(svc)) obj2 := roundTrip(t, runtime.Object(svc))
svc2 := obj2.(*current.Service) svc2 := obj2.(*current.Service)
if svc2.Spec.Protocol != current.ProtocolTCP {
t.Errorf("Expected default protocol :%s, got: %s", current.ProtocolTCP, svc2.Spec.Protocol)
}
if svc2.Spec.SessionAffinity != current.AffinityTypeNone { if svc2.Spec.SessionAffinity != current.AffinityTypeNone {
t.Errorf("Expected default sesseion affinity type:%s, got: %s", current.AffinityTypeNone, svc2.Spec.SessionAffinity) t.Errorf("Expected default sesseion affinity type:%s, got: %s", current.AffinityTypeNone, svc2.Spec.SessionAffinity)
} }
@@ -85,18 +82,62 @@ func TestSetDefaulEndpointsProtocol(t *testing.T) {
} }
func TestSetDefaulServiceTargetPort(t *testing.T) { func TestSetDefaulServiceTargetPort(t *testing.T) {
in := &current.Service{Spec: current.ServiceSpec{Port: 1234}} in := &current.Service{Spec: current.ServiceSpec{Ports: []current.ServicePort{{Port: 1234}}}}
obj := roundTrip(t, runtime.Object(in)) obj := roundTrip(t, runtime.Object(in))
out := obj.(*current.Service) out := obj.(*current.Service)
if out.Spec.TargetPort.Kind != util.IntstrInt || out.Spec.TargetPort.IntVal != 1234 { if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromInt(1234) {
t.Errorf("Expected TargetPort to be defaulted, got %s", out.Spec.TargetPort) t.Errorf("Expected TargetPort to be defaulted, got %s", out.Spec.Ports[0].TargetPort)
} }
in = &current.Service{Spec: current.ServiceSpec{Port: 1234, TargetPort: util.NewIntOrStringFromInt(5678)}} in = &current.Service{Spec: current.ServiceSpec{Ports: []current.ServicePort{{Port: 1234, TargetPort: util.NewIntOrStringFromInt(5678)}}}}
obj = roundTrip(t, runtime.Object(in)) obj = roundTrip(t, runtime.Object(in))
out = obj.(*current.Service) out = obj.(*current.Service)
if out.Spec.TargetPort.Kind != util.IntstrInt || out.Spec.TargetPort.IntVal != 5678 { if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromInt(5678) {
t.Errorf("Expected TargetPort to be unchanged, got %s", out.Spec.TargetPort) t.Errorf("Expected TargetPort to be unchanged, got %s", out.Spec.Ports[0].TargetPort)
}
}
func TestSetDefaultServicePort(t *testing.T) {
// Unchanged if set.
in := &current.Service{Spec: current.ServiceSpec{
Ports: []current.ServicePort{
{Protocol: "UDP", Port: 9376, TargetPort: util.NewIntOrStringFromString("p")},
{Protocol: "UDP", Port: 8675, TargetPort: util.NewIntOrStringFromInt(309)},
},
}}
out := roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Spec.Ports[0].Protocol != current.ProtocolUDP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Spec.Ports[0].Protocol)
}
if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromString("p") {
t.Errorf("Expected port %d, got %s", in.Spec.Ports[0].Port, out.Spec.Ports[0].TargetPort)
}
if out.Spec.Ports[1].Protocol != current.ProtocolUDP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolUDP, out.Spec.Ports[1].Protocol)
}
if out.Spec.Ports[1].TargetPort != util.NewIntOrStringFromInt(309) {
t.Errorf("Expected port %d, got %s", in.Spec.Ports[1].Port, out.Spec.Ports[1].TargetPort)
}
// Defaulted.
in = &current.Service{Spec: current.ServiceSpec{
Ports: []current.ServicePort{
{Protocol: "", Port: 9376, TargetPort: util.NewIntOrStringFromString("")},
{Protocol: "", Port: 8675, TargetPort: util.NewIntOrStringFromInt(0)},
},
}}
out = roundTrip(t, runtime.Object(in)).(*current.Service)
if out.Spec.Ports[0].Protocol != current.ProtocolTCP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Spec.Ports[0].Protocol)
}
if out.Spec.Ports[0].TargetPort != util.NewIntOrStringFromInt(in.Spec.Ports[0].Port) {
t.Errorf("Expected port %d, got %d", in.Spec.Ports[0].Port, out.Spec.Ports[0].TargetPort)
}
if out.Spec.Ports[1].Protocol != current.ProtocolTCP {
t.Errorf("Expected protocol %s, got %s", current.ProtocolTCP, out.Spec.Ports[1].Protocol)
}
if out.Spec.Ports[1].TargetPort != util.NewIntOrStringFromInt(in.Spec.Ports[1].Port) {
t.Errorf("Expected port %d, got %d", in.Spec.Ports[1].Port, out.Spec.Ports[1].TargetPort)
} }
} }

View File

@@ -853,12 +853,8 @@ type ServiceStatus struct{}
// ServiceSpec describes the attributes that a user creates on a service // ServiceSpec describes the attributes that a user creates on a service
type ServiceSpec struct { type ServiceSpec struct {
// Port is the TCP or UDP port that will be made available to each pod for connecting to the pods // Required: The list of ports that are exposed by this service.
// proxied by this service. Ports []ServicePort `json:"ports" description:"ports exposed by the service"`
Port int `json:"port" description:"port exposed by the service"`
// Optional: Supports "TCP" and "UDP". Defaults to "TCP".
Protocol Protocol `json:"protocol,omitempty" description:"protocol for port; must be UDP or TCP; TCP if unspecified"`
// This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected. // This service will route traffic to pods having labels matching this selector. If null, no endpoints will be automatically created. If empty, all pods will be selected.
Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"` Selector map[string]string `json:"selector" description:"label keys and values that must match in order to receive traffic for this service; if empty, all pods are selected, if not specified, endpoints must be manually specified"`
@@ -877,15 +873,31 @@ type ServiceSpec struct {
// users to handle external traffic that arrives at a node. // users to handle external traffic that arrives at a node.
PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"`
// TargetPort is the name or number of the port on the container to direct traffic to.
// This is useful if the containers the service points to have multiple open ports.
// Optional: If unspecified, the service port is used (an identity map).
TargetPort util.IntOrString `json:"targetPort,omitempty" description:"number or name of the port to access on the containers belonging to pods targeted by the service; defaults to the container's first open port"`
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity. // Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"` SessionAffinity AffinityType `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
} }
type ServicePort struct {
// Optional if only one ServicePort is defined on this service: The
// name of this port within the service. This must be a DNS_LABEL.
// All ports within a ServiceSpec must have unique names. This maps to
// the 'Name' field in EndpointPort objects.
Name string `json:"name" description:"the name of this port; optional if only one port is defined"`
// Optional: The IP protocol for this port. Supports "TCP" and "UDP",
// default is TCP.
Protocol Protocol `json:"protocol" description:"the protocol used by this port; must be UDP or TCP; TCP if unspecified"`
// Required: The port that will be exposed by this service.
Port int `json:"port" description:"the port number that is exposed"`
// Optional: The target port on pods selected by this service.
// If this is a string, it will be looked up as a named port in the
// target Pod's container ports. If this is not specified, the value
// of Port is used (an identity map).
TargetPort util.IntOrString `json:"targetPort" description:"the port to access on the pods targeted by the service; defaults to the service port"`
}
// Service is a named abstraction of software service (for example, mysql) consisting of local port // Service is a named abstraction of software service (for example, mysql) consisting of local port
// (for example 3306) that the proxy listens on, and the selector that determines which pods // (for example 3306) that the proxy listens on, and the selector that determines which pods
// will answer requests sent through the proxy. // will answer requests sent through the proxy.

View File

@@ -786,18 +786,12 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{} allErrs := errs.ValidationErrorList{}
allErrs = append(allErrs, ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName).Prefix("metadata")...) allErrs = append(allErrs, ValidateObjectMeta(&service.ObjectMeta, true, ValidateServiceName).Prefix("metadata")...)
if !util.IsValidPortNum(service.Spec.Port) { if len(service.Spec.Ports) == 0 {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.port", service.Spec.Port, portRangeErrorMsg)) allErrs = append(allErrs, errs.NewFieldRequired("spec.ports"))
} }
if len(service.Spec.Protocol) == 0 { allPortNames := util.StringSet{}
allErrs = append(allErrs, errs.NewFieldRequired("spec.protocol")) for i := range service.Spec.Ports {
} else if !supportedPortProtocols.Has(strings.ToUpper(string(service.Spec.Protocol))) { allErrs = append(allErrs, validateServicePort(&service.Spec.Ports[i], i, &allPortNames).PrefixIndex(i).Prefix("spec.ports")...)
allErrs = append(allErrs, errs.NewFieldNotSupported("spec.protocol", service.Spec.Protocol))
}
if service.Spec.TargetPort.Kind == util.IntstrInt && service.Spec.TargetPort.IntVal != 0 && !util.IsValidPortNum(service.Spec.TargetPort.IntVal) {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.containerPort", service.Spec.Port, portRangeErrorMsg))
} else if service.Spec.TargetPort.Kind == util.IntstrString && len(service.Spec.TargetPort.StrVal) == 0 {
allErrs = append(allErrs, errs.NewFieldRequired("spec.containerPort"))
} }
if service.Spec.Selector != nil { if service.Spec.Selector != nil {
@@ -827,6 +821,39 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
return allErrs return allErrs
} }
func validateServicePort(sp *api.ServicePort, index int, allNames *util.StringSet) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
if len(sp.Name) == 0 {
// Allow empty names if they are the first port (mostly for compat).
if index != 0 {
allErrs = append(allErrs, errs.NewFieldRequired("name"))
}
} else if !util.IsDNS1123Label(sp.Name) {
allErrs = append(allErrs, errs.NewFieldInvalid("name", sp.Name, dns1123LabelErrorMsg))
} else if allNames.Has(sp.Name) {
allErrs = append(allErrs, errs.NewFieldDuplicate("name", sp.Name))
}
if !util.IsValidPortNum(sp.Port) {
allErrs = append(allErrs, errs.NewFieldInvalid("port", sp.Port, portRangeErrorMsg))
}
if len(sp.Protocol) == 0 {
allErrs = append(allErrs, errs.NewFieldRequired("protocol"))
} else if !supportedPortProtocols.Has(strings.ToUpper(string(sp.Protocol))) {
allErrs = append(allErrs, errs.NewFieldNotSupported("protocol", sp.Protocol))
}
if sp.TargetPort != util.NewIntOrStringFromInt(0) && sp.TargetPort != util.NewIntOrStringFromString("") {
if sp.TargetPort.Kind == util.IntstrInt && !util.IsValidPortNum(sp.TargetPort.IntVal) {
allErrs = append(allErrs, errs.NewFieldInvalid("targetPort", sp.TargetPort, portRangeErrorMsg))
}
}
return allErrs
}
// ValidateServiceUpdate tests if required fields in the service are set during an update // ValidateServiceUpdate tests if required fields in the service are set during an update
func ValidateServiceUpdate(oldService, service *api.Service) errs.ValidationErrorList { func ValidateServiceUpdate(oldService, service *api.Service) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{} allErrs := errs.ValidationErrorList{}

View File

@@ -1292,6 +1292,13 @@ func TestValidateService(t *testing.T) {
}, },
numErrs: 1, numErrs: 1,
}, },
{
name: "nil selector",
makeSvc: func(s *api.Service) {
s.Spec.Selector = nil
},
numErrs: 0,
},
{ {
name: "invalid selector", name: "invalid selector",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
@@ -1306,17 +1313,45 @@ func TestValidateService(t *testing.T) {
}, },
numErrs: 1, numErrs: 1,
}, },
{
name: "missing ports",
makeSvc: func(s *api.Service) {
s.Spec.Ports = nil
},
numErrs: 1,
},
{
name: "empty port[0] name",
makeSvc: func(s *api.Service) {
s.Spec.Ports[0].Name = ""
},
numErrs: 0,
},
{
name: "empty port[1] name",
makeSvc: func(s *api.Service) {
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "", Protocol: "TCP", Port: 12345})
},
numErrs: 1,
},
{
name: "invalid port name",
makeSvc: func(s *api.Service) {
s.Spec.Ports[0].Name = "INVALID"
},
numErrs: 1,
},
{ {
name: "missing protocol", name: "missing protocol",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.Protocol = "" s.Spec.Ports[0].Protocol = ""
}, },
numErrs: 1, numErrs: 1,
}, },
{ {
name: "invalid protocol", name: "invalid protocol",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.Protocol = "INVALID" s.Spec.Ports[0].Protocol = "INVALID"
}, },
numErrs: 1, numErrs: 1,
}, },
@@ -1330,28 +1365,21 @@ func TestValidateService(t *testing.T) {
{ {
name: "missing port", name: "missing port",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.Port = 0 s.Spec.Ports[0].Port = 0
}, },
numErrs: 1, numErrs: 1,
}, },
{ {
name: "invalid port", name: "invalid port",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.Port = 65536 s.Spec.Ports[0].Port = 65536
}, },
numErrs: 1, numErrs: 1,
}, },
{ {
name: "missing targetPort string", name: "invalid TargetPort int",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.TargetPort = util.NewIntOrStringFromString("") s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(65536)
},
numErrs: 1,
},
{
name: "invalid targetPort int",
makeSvc: func(s *api.Service) {
s.Spec.TargetPort = util.NewIntOrStringFromInt(65536)
}, },
numErrs: 1, numErrs: 1,
}, },
@@ -1377,11 +1405,12 @@ func TestValidateService(t *testing.T) {
numErrs: 0, numErrs: 0,
}, },
{ {
name: "nil selector", name: "dup port name",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.Selector = nil s.Spec.Ports[0].Name = "p"
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p", Port: 12345})
}, },
numErrs: 0, numErrs: 1,
}, },
{ {
name: "valid 1", name: "valid 1",
@@ -1393,15 +1422,15 @@ func TestValidateService(t *testing.T) {
{ {
name: "valid 2", name: "valid 2",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.Protocol = "UDP" s.Spec.Ports[0].Protocol = "UDP"
s.Spec.TargetPort = util.NewIntOrStringFromInt(12345) s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(12345)
}, },
numErrs: 0, numErrs: 0,
}, },
{ {
name: "valid 3", name: "valid 3",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.TargetPort = util.NewIntOrStringFromString("http") s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromString("http")
}, },
numErrs: 0, numErrs: 0,
}, },
@@ -1416,6 +1445,7 @@ func TestValidateService(t *testing.T) {
name: "valid portal ip - empty", name: "valid portal ip - empty",
makeSvc: func(s *api.Service) { makeSvc: func(s *api.Service) {
s.Spec.PortalIP = "" s.Spec.PortalIP = ""
s.Spec.Ports[0].TargetPort = util.NewIntOrStringFromString("http")
}, },
numErrs: 0, numErrs: 0,
}, },
@@ -1432,8 +1462,7 @@ func TestValidateService(t *testing.T) {
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"key": "val"}, Selector: map[string]string{"key": "val"},
SessionAffinity: "None", SessionAffinity: "None",
Port: 8675, Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}},
Protocol: "TCP",
}, },
} }
tc.makeSvc(&svc) tc.makeSvc(&svc)

View File

@@ -694,7 +694,7 @@ func TestRequestDo(t *testing.T) {
func TestDoRequestNewWay(t *testing.T) { func TestDoRequestNewWay(t *testing.T) {
reqBody := "request body" reqBody := "request body"
expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 12345}}}}
expectedBody, _ := v1beta2.Codec.Encode(expectedObj) expectedBody, _ := v1beta2.Codec.Encode(expectedObj)
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
@@ -728,7 +728,7 @@ func TestDoRequestNewWay(t *testing.T) {
func TestDoRequestNewWayReader(t *testing.T) { func TestDoRequestNewWayReader(t *testing.T) {
reqObj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} reqObj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}
reqBodyExpected, _ := v1beta1.Codec.Encode(reqObj) reqBodyExpected, _ := v1beta1.Codec.Encode(reqObj)
expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 12345}}}}
expectedBody, _ := v1beta1.Codec.Encode(expectedObj) expectedBody, _ := v1beta1.Codec.Encode(expectedObj)
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
@@ -764,7 +764,7 @@ func TestDoRequestNewWayReader(t *testing.T) {
func TestDoRequestNewWayObj(t *testing.T) { func TestDoRequestNewWayObj(t *testing.T) {
reqObj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}} reqObj := &api.Pod{ObjectMeta: api.ObjectMeta{Name: "foo"}}
reqBodyExpected, _ := v1beta2.Codec.Encode(reqObj) reqBodyExpected, _ := v1beta2.Codec.Encode(reqObj)
expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 12345}}}}
expectedBody, _ := v1beta2.Codec.Encode(expectedObj) expectedBody, _ := v1beta2.Codec.Encode(expectedObj)
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
@@ -814,7 +814,7 @@ func TestDoRequestNewWayFile(t *testing.T) {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 12345}}}}
expectedBody, _ := v1beta1.Codec.Encode(expectedObj) expectedBody, _ := v1beta1.Codec.Encode(expectedObj)
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 200, StatusCode: 200,
@@ -855,7 +855,7 @@ func TestWasCreated(t *testing.T) {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
expectedObj := &api.Service{Spec: api.ServiceSpec{Port: 12345}} expectedObj := &api.Service{Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 12345}}}}
expectedBody, _ := v1beta1.Codec.Encode(expectedObj) expectedBody, _ := v1beta1.Codec.Encode(expectedObj)
fakeHandler := util.FakeHandler{ fakeHandler := util.FakeHandler{
StatusCode: 201, StatusCode: 201,

View File

@@ -48,7 +48,7 @@ type TCPLoadBalancer interface {
// TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service // TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service
TCPLoadBalancerExists(name, region string) (bool, error) TCPLoadBalancerExists(name, region string) (bool, error)
// CreateTCPLoadBalancer creates a new tcp load balancer. Returns the IP address or hostname of the balancer // CreateTCPLoadBalancer creates a new tcp load balancer. Returns the IP address or hostname of the balancer
CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.AffinityType) (string, error)
// UpdateTCPLoadBalancer updates hosts under the specified load balancer. // UpdateTCPLoadBalancer updates hosts under the specified load balancer.
UpdateTCPLoadBalancer(name, region string, hosts []string) error UpdateTCPLoadBalancer(name, region string, hosts []string) error
// DeleteTCPLoadBalancer deletes a specified load balancer. // DeleteTCPLoadBalancer deletes a specified load balancer.

View File

@@ -29,7 +29,7 @@ type FakeBalancer struct {
Name string Name string
Region string Region string
ExternalIP net.IP ExternalIP net.IP
Port int Ports []int
Hosts []string Hosts []string
} }
@@ -95,9 +95,9 @@ func (f *FakeCloud) TCPLoadBalancerExists(name, region string) (bool, error) {
// CreateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.CreateTCPLoadBalancer. // CreateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
// It adds an entry "create" into the internal method call record. // It adds an entry "create" into the internal method call record.
func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) { func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.AffinityType) (string, error) {
f.addCall("create") f.addCall("create")
f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, port, hosts}) f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, ports, hosts})
return f.ExternalIP.String(), f.Err return f.ExternalIP.String(), f.Err
} }

View File

@@ -228,15 +228,29 @@ func translateAffinityType(affinityType api.AffinityType) GCEAffinityType {
} }
// CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer. // CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinityType api.AffinityType) (string, error) { func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.AffinityType) (string, error) {
pool, err := gce.makeTargetPool(name, region, hosts, translateAffinityType(affinityType)) pool, err := gce.makeTargetPool(name, region, hosts, translateAffinityType(affinityType))
if err != nil { if err != nil {
return "", err return "", err
} }
if len(ports) == 0 {
return "", fmt.Errorf("no ports specified for GCE load balancer")
}
minPort := 65536
maxPort := 0
for i := range ports {
if ports[i] < minPort {
minPort = ports[i]
}
if ports[i] > maxPort {
maxPort = ports[i]
}
}
req := &compute.ForwardingRule{ req := &compute.ForwardingRule{
Name: name, Name: name,
IPProtocol: "TCP", IPProtocol: "TCP",
PortRange: strconv.Itoa(port), PortRange: fmt.Sprintf("%d-%d", minPort, maxPort),
Target: pool, Target: pool,
} }
if len(externalIP) > 0 { if len(externalIP) > 0 {

View File

@@ -485,8 +485,8 @@ func (lb *LoadBalancer) TCPLoadBalancerExists(name, region string) (bool, error)
// a list of regions (from config) and query/create loadbalancers in // a list of regions (from config) and query/create loadbalancers in
// each region. // each region.
func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, port int, hosts []string, affinity api.AffinityType) (string, error) { func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinity api.AffinityType) (string, error) {
glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, port, hosts, affinity) glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, ports, hosts, affinity)
var persistence *vips.SessionPersistence var persistence *vips.SessionPersistence
switch affinity { switch affinity {
@@ -515,7 +515,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
_, err = members.Create(lb.network, members.CreateOpts{ _, err = members.Create(lb.network, members.CreateOpts{
PoolID: pool.ID, PoolID: pool.ID,
ProtocolPort: port, ProtocolPort: ports[0], //FIXME: need to handle multi-port
Address: addr, Address: addr,
}).Extract() }).Extract()
if err != nil { if err != nil {
@@ -550,7 +550,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
Description: fmt.Sprintf("Kubernetes external service %s", name), Description: fmt.Sprintf("Kubernetes external service %s", name),
Address: externalIP.String(), Address: externalIP.String(),
Protocol: "TCP", Protocol: "TCP",
ProtocolPort: port, ProtocolPort: ports[0], //FIXME: need to handle multi-port
PoolID: pool.ID, PoolID: pool.ID,
Persistence: persistence, Persistence: persistence,
}).Extract() }).Extract()

View File

@@ -65,7 +65,6 @@ func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList)
{ {
ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Protocol: "TCP",
SessionAffinity: "None", SessionAffinity: "None",
}, },
}, },

View File

@@ -135,15 +135,11 @@ func TestMerge(t *testing.T) {
{ {
kind: "Service", kind: "Service",
obj: &api.Service{ obj: &api.Service{
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{},
Port: 10,
},
}, },
fragment: `{ "apiVersion": "v1beta1", "port": 0 }`, fragment: `{ "apiVersion": "v1beta1", "port": 0 }`,
expected: &api.Service{ expected: &api.Service{
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 0,
Protocol: "TCP",
SessionAffinity: "None", SessionAffinity: "None",
}, },
}, },
@@ -160,7 +156,6 @@ func TestMerge(t *testing.T) {
fragment: `{ "apiVersion": "v1beta1", "selector": { "version": "v2" } }`, fragment: `{ "apiVersion": "v1beta1", "selector": { "version": "v2" } }`,
expected: &api.Service{ expected: &api.Service{
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Protocol: "TCP",
SessionAffinity: "None", SessionAffinity: "None",
Selector: map[string]string{ Selector: map[string]string{
"version": "v2", "version": "v2",

View File

@@ -333,7 +333,15 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api
list := strings.Join(service.Spec.PublicIPs, ", ") list := strings.Join(service.Spec.PublicIPs, ", ")
fmt.Fprintf(out, "Public IPs:\t%s\n", list) fmt.Fprintf(out, "Public IPs:\t%s\n", list)
} }
fmt.Fprintf(out, "Port:\t%d\n", service.Spec.Port) for i := range service.Spec.Ports {
sp := &service.Spec.Ports[i]
name := sp.Name
if name == "" {
name = "<unnamed>"
}
fmt.Fprintf(out, "Port:\t%s\t%d/%s\n", sp.Name, sp.Port, sp.Protocol)
}
fmt.Fprintf(out, "Endpoints:\t%s\n", formatEndpoints(endpoints)) fmt.Fprintf(out, "Endpoints:\t%s\n", formatEndpoints(endpoints))
fmt.Fprintf(out, "Session Affinity:\t%s\n", service.Spec.SessionAffinity) fmt.Fprintf(out, "Session Affinity:\t%s\n", service.Spec.SessionAffinity)
if events != nil { if events != nil {

View File

@@ -228,7 +228,7 @@ func (h *HumanReadablePrinter) validatePrintHandlerFunc(printFunc reflect.Value)
var podColumns = []string{"POD", "IP", "CONTAINER(S)", "IMAGE(S)", "HOST", "LABELS", "STATUS", "CREATED"} var podColumns = []string{"POD", "IP", "CONTAINER(S)", "IMAGE(S)", "HOST", "LABELS", "STATUS", "CREATED"}
var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS"} var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS"}
var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT"} var serviceColumns = []string{"NAME", "LABELS", "SELECTOR", "IP", "PORT(S)"}
var endpointColumns = []string{"NAME", "ENDPOINTS"} var endpointColumns = []string{"NAME", "ENDPOINTS"}
var nodeColumns = []string{"NAME", "LABELS", "STATUS"} var nodeColumns = []string{"NAME", "LABELS", "STATUS"}
var statusColumns = []string{"STATUS"} var statusColumns = []string{"STATUS"}
@@ -390,9 +390,18 @@ func printReplicationControllerList(list *api.ReplicationControllerList, w io.Wr
} }
func printService(svc *api.Service, w io.Writer) error { func printService(svc *api.Service, w io.Writer) error {
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", svc.Name, formatLabels(svc.Labels), if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", svc.Name, formatLabels(svc.Labels),
formatLabels(svc.Spec.Selector), svc.Spec.PortalIP, svc.Spec.Port) formatLabels(svc.Spec.Selector), svc.Spec.PortalIP, svc.Spec.Ports[0].Port, svc.Spec.Ports[0].Protocol); err != nil {
return err return err
}
for i := 1; i < len(svc.Spec.Ports); i++ {
// Lay out additional ports.
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", "", "", "", "", svc.Spec.Ports[i].Port, svc.Spec.Ports[i].Protocol); err != nil {
return err
}
}
return nil
} }
func printServiceList(list *api.ServiceList, w io.Writer) error { func printServiceList(list *api.ServiceList, w io.Writer) error {

View File

@@ -71,9 +71,14 @@ func (ServiceGenerator) Generate(params map[string]string) (runtime.Object, erro
Labels: labels, Labels: labels,
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: port,
Protocol: api.Protocol(params["protocol"]),
Selector: selector, Selector: selector,
Ports: []api.ServicePort{
{
Name: "default",
Port: port,
Protocol: api.Protocol(params["protocol"]),
},
},
}, },
} }
targetPort, found := params["target-port"] targetPort, found := params["target-port"]
@@ -82,12 +87,12 @@ func (ServiceGenerator) Generate(params map[string]string) (runtime.Object, erro
} }
if found && len(targetPort) > 0 { if found && len(targetPort) > 0 {
if portNum, err := strconv.Atoi(targetPort); err != nil { if portNum, err := strconv.Atoi(targetPort); err != nil {
service.Spec.TargetPort = util.NewIntOrStringFromString(targetPort) service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromString(targetPort)
} else { } else {
service.Spec.TargetPort = util.NewIntOrStringFromInt(portNum) service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(portNum)
} }
} else { } else {
service.Spec.TargetPort = util.NewIntOrStringFromInt(port) service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(port)
} }
if params["create-external-load-balancer"] == "true" { if params["create-external-load-balancer"] == "true" {
service.Spec.CreateExternalLoadBalancer = true service.Spec.CreateExternalLoadBalancer = true

View File

@@ -46,9 +46,14 @@ func TestGenerateService(t *testing.T) {
"foo": "bar", "foo": "bar",
"baz": "blah", "baz": "blah",
}, },
Port: 80, Ports: []api.ServicePort{
Protocol: "TCP", {
TargetPort: util.NewIntOrStringFromInt(1234), Name: "default",
Port: 80,
Protocol: "TCP",
TargetPort: util.NewIntOrStringFromInt(1234),
},
},
}, },
}, },
}, },
@@ -69,9 +74,14 @@ func TestGenerateService(t *testing.T) {
"foo": "bar", "foo": "bar",
"baz": "blah", "baz": "blah",
}, },
Port: 80, Ports: []api.ServicePort{
Protocol: "UDP", {
TargetPort: util.NewIntOrStringFromString("foobar"), Name: "default",
Port: 80,
Protocol: "UDP",
TargetPort: util.NewIntOrStringFromString("foobar"),
},
},
}, },
}, },
}, },
@@ -97,9 +107,14 @@ func TestGenerateService(t *testing.T) {
"foo": "bar", "foo": "bar",
"baz": "blah", "baz": "blah",
}, },
Port: 80, Ports: []api.ServicePort{
Protocol: "TCP", {
TargetPort: util.NewIntOrStringFromInt(1234), Name: "default",
Port: 80,
Protocol: "TCP",
TargetPort: util.NewIntOrStringFromInt(1234),
},
},
}, },
}, },
}, },
@@ -121,10 +136,15 @@ func TestGenerateService(t *testing.T) {
"foo": "bar", "foo": "bar",
"baz": "blah", "baz": "blah",
}, },
Port: 80, Ports: []api.ServicePort{
Protocol: "UDP", {
PublicIPs: []string{"1.2.3.4"}, Name: "default",
TargetPort: util.NewIntOrStringFromString("foobar"), Port: 80,
Protocol: "UDP",
TargetPort: util.NewIntOrStringFromString("foobar"),
},
},
PublicIPs: []string{"1.2.3.4"},
}, },
}, },
}, },
@@ -147,10 +167,15 @@ func TestGenerateService(t *testing.T) {
"foo": "bar", "foo": "bar",
"baz": "blah", "baz": "blah",
}, },
Port: 80, Ports: []api.ServicePort{
Protocol: "UDP", {
Name: "default",
Port: 80,
Protocol: "UDP",
TargetPort: util.NewIntOrStringFromString("foobar"),
},
},
PublicIPs: []string{"1.2.3.4"}, PublicIPs: []string{"1.2.3.4"},
TargetPort: util.NewIntOrStringFromString("foobar"),
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
}, },
}, },

View File

@@ -29,19 +29,30 @@ import (
// provided as an argument. // provided as an argument.
func FromServices(services *api.ServiceList) []api.EnvVar { func FromServices(services *api.ServiceList) []api.EnvVar {
var result []api.EnvVar var result []api.EnvVar
for _, service := range services.Items { for i := range services.Items {
service := &services.Items[i]
// ignore services where PortalIP is "None" or empty // ignore services where PortalIP is "None" or empty
// the services passed to this method should be pre-filtered // the services passed to this method should be pre-filtered
// only services that have the portal IP set should be included here // only services that have the portal IP set should be included here
if !api.IsServiceIPSet(&service) { if !api.IsServiceIPSet(service) {
continue continue
} }
// Host // Host
name := makeEnvVariableName(service.Name) + "_SERVICE_HOST" name := makeEnvVariableName(service.Name) + "_SERVICE_HOST"
result = append(result, api.EnvVar{Name: name, Value: service.Spec.PortalIP}) result = append(result, api.EnvVar{Name: name, Value: service.Spec.PortalIP})
// Port // First port - give it the backwards-compatible name
name = makeEnvVariableName(service.Name) + "_SERVICE_PORT" name = makeEnvVariableName(service.Name) + "_SERVICE_PORT"
result = append(result, api.EnvVar{Name: name, Value: strconv.Itoa(service.Spec.Port)}) result = append(result, api.EnvVar{Name: name, Value: strconv.Itoa(service.Spec.Ports[0].Port)})
// All named ports (only the first may be unnamed, checked in validation)
for i := range service.Spec.Ports {
sp := &service.Spec.Ports[i]
if sp.Name != "" {
pn := name + "_" + makeEnvVariableName(sp.Name)
result = append(result, api.EnvVar{Name: pn, Value: strconv.Itoa(sp.Port)})
}
}
// Docker-compatible vars. // Docker-compatible vars.
result = append(result, makeLinkVariables(service)...) result = append(result, makeLinkVariables(service)...)
} }
@@ -56,33 +67,42 @@ func makeEnvVariableName(str string) string {
return strings.ToUpper(strings.Replace(str, "-", "_", -1)) return strings.ToUpper(strings.Replace(str, "-", "_", -1))
} }
func makeLinkVariables(service api.Service) []api.EnvVar { func makeLinkVariables(service *api.Service) []api.EnvVar {
prefix := makeEnvVariableName(service.Name) prefix := makeEnvVariableName(service.Name)
protocol := string(api.ProtocolTCP) all := []api.EnvVar{}
if service.Spec.Protocol != "" { for i := range service.Spec.Ports {
protocol = string(service.Spec.Protocol) sp := &service.Spec.Ports[i]
}
portPrefix := fmt.Sprintf("%s_PORT_%d_%s", prefix, service.Spec.Port, strings.ToUpper(protocol)) protocol := string(api.ProtocolTCP)
return []api.EnvVar{ if sp.Protocol != "" {
{ protocol = string(sp.Protocol)
Name: prefix + "_PORT", }
Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, service.Spec.Port), if i == 0 {
}, // Docker special-cases the first port.
{ all = append(all, api.EnvVar{
Name: portPrefix, Name: prefix + "_PORT",
Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, service.Spec.Port), Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, sp.Port),
}, })
{ }
Name: portPrefix + "_PROTO", portPrefix := fmt.Sprintf("%s_PORT_%d_%s", prefix, sp.Port, strings.ToUpper(protocol))
Value: strings.ToLower(protocol), all = append(all, []api.EnvVar{
}, {
{ Name: portPrefix,
Name: portPrefix + "_PORT", Value: fmt.Sprintf("%s://%s:%d", strings.ToLower(protocol), service.Spec.PortalIP, sp.Port),
Value: strconv.Itoa(service.Spec.Port), },
}, {
{ Name: portPrefix + "_PROTO",
Name: portPrefix + "_ADDR", Value: strings.ToLower(protocol),
Value: service.Spec.PortalIP, },
}, {
Name: portPrefix + "_PORT",
Value: strconv.Itoa(sp.Port),
},
{
Name: portPrefix + "_ADDR",
Value: service.Spec.PortalIP,
},
}...)
} }
return all
} }

View File

@@ -30,46 +30,53 @@ func TestFromServices(t *testing.T) {
{ {
ObjectMeta: api.ObjectMeta{Name: "foo-bar"}, ObjectMeta: api.ObjectMeta{Name: "foo-bar"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8080,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: "TCP",
PortalIP: "1.2.3.4", PortalIP: "1.2.3.4",
Ports: []api.ServicePort{
{Port: 8080, Protocol: "TCP"},
},
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "abc-123"}, ObjectMeta: api.ObjectMeta{Name: "abc-123"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8081,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: "UDP",
PortalIP: "5.6.7.8", PortalIP: "5.6.7.8",
Ports: []api.ServicePort{
{Name: "u-d-p", Port: 8081, Protocol: "UDP"},
{Name: "t-c-p", Port: 8081, Protocol: "TCP"},
},
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "q-u-u-x"}, ObjectMeta: api.ObjectMeta{Name: "q-u-u-x"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8082,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: "TCP",
PortalIP: "9.8.7.6", PortalIP: "9.8.7.6",
Ports: []api.ServicePort{
{Port: 8082, Protocol: "TCP"},
{Name: "8083", Port: 8083, Protocol: "TCP"},
},
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-none"}, ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-none"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8082,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: "TCP",
PortalIP: "None", PortalIP: "None",
Ports: []api.ServicePort{
{Port: 8082, Protocol: "TCP"},
},
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-empty"}, ObjectMeta: api.ObjectMeta{Name: "svrc-portalip-empty"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8082,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: "TCP",
PortalIP: "", PortalIP: "",
Ports: []api.ServicePort{
{Port: 8082, Protocol: "TCP"},
},
}, },
}, },
}, },
@@ -85,18 +92,29 @@ func TestFromServices(t *testing.T) {
{Name: "FOO_BAR_PORT_8080_TCP_ADDR", Value: "1.2.3.4"}, {Name: "FOO_BAR_PORT_8080_TCP_ADDR", Value: "1.2.3.4"},
{Name: "ABC_123_SERVICE_HOST", Value: "5.6.7.8"}, {Name: "ABC_123_SERVICE_HOST", Value: "5.6.7.8"},
{Name: "ABC_123_SERVICE_PORT", Value: "8081"}, {Name: "ABC_123_SERVICE_PORT", Value: "8081"},
{Name: "ABC_123_SERVICE_PORT_U_D_P", Value: "8081"},
{Name: "ABC_123_SERVICE_PORT_T_C_P", Value: "8081"},
{Name: "ABC_123_PORT", Value: "udp://5.6.7.8:8081"}, {Name: "ABC_123_PORT", Value: "udp://5.6.7.8:8081"},
{Name: "ABC_123_PORT_8081_UDP", Value: "udp://5.6.7.8:8081"}, {Name: "ABC_123_PORT_8081_UDP", Value: "udp://5.6.7.8:8081"},
{Name: "ABC_123_PORT_8081_UDP_PROTO", Value: "udp"}, {Name: "ABC_123_PORT_8081_UDP_PROTO", Value: "udp"},
{Name: "ABC_123_PORT_8081_UDP_PORT", Value: "8081"}, {Name: "ABC_123_PORT_8081_UDP_PORT", Value: "8081"},
{Name: "ABC_123_PORT_8081_UDP_ADDR", Value: "5.6.7.8"}, {Name: "ABC_123_PORT_8081_UDP_ADDR", Value: "5.6.7.8"},
{Name: "ABC_123_PORT_8081_TCP", Value: "tcp://5.6.7.8:8081"},
{Name: "ABC_123_PORT_8081_TCP_PROTO", Value: "tcp"},
{Name: "ABC_123_PORT_8081_TCP_PORT", Value: "8081"},
{Name: "ABC_123_PORT_8081_TCP_ADDR", Value: "5.6.7.8"},
{Name: "Q_U_U_X_SERVICE_HOST", Value: "9.8.7.6"}, {Name: "Q_U_U_X_SERVICE_HOST", Value: "9.8.7.6"},
{Name: "Q_U_U_X_SERVICE_PORT", Value: "8082"}, {Name: "Q_U_U_X_SERVICE_PORT", Value: "8082"},
{Name: "Q_U_U_X_SERVICE_PORT_8083", Value: "8083"},
{Name: "Q_U_U_X_PORT", Value: "tcp://9.8.7.6:8082"}, {Name: "Q_U_U_X_PORT", Value: "tcp://9.8.7.6:8082"},
{Name: "Q_U_U_X_PORT_8082_TCP", Value: "tcp://9.8.7.6:8082"}, {Name: "Q_U_U_X_PORT_8082_TCP", Value: "tcp://9.8.7.6:8082"},
{Name: "Q_U_U_X_PORT_8082_TCP_PROTO", Value: "tcp"}, {Name: "Q_U_U_X_PORT_8082_TCP_PROTO", Value: "tcp"},
{Name: "Q_U_U_X_PORT_8082_TCP_PORT", Value: "8082"}, {Name: "Q_U_U_X_PORT_8082_TCP_PORT", Value: "8082"},
{Name: "Q_U_U_X_PORT_8082_TCP_ADDR", Value: "9.8.7.6"}, {Name: "Q_U_U_X_PORT_8082_TCP_ADDR", Value: "9.8.7.6"},
{Name: "Q_U_U_X_PORT_8083_TCP", Value: "tcp://9.8.7.6:8083"},
{Name: "Q_U_U_X_PORT_8083_TCP_PROTO", Value: "tcp"},
{Name: "Q_U_U_X_PORT_8083_TCP_PORT", Value: "8083"},
{Name: "Q_U_U_X_PORT_8083_TCP_ADDR", Value: "9.8.7.6"},
} }
if len(vars) != len(expected) { if len(vars) != len(expected) {
t.Errorf("Expected %d env vars, got: %+v", len(expected), vars) t.Errorf("Expected %d env vars, got: %+v", len(expected), vars)

View File

@@ -1787,97 +1787,139 @@ func TestMakeEnvironmentVariables(t *testing.T) {
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8081, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8081,
}},
PortalIP: "1.2.3.1", PortalIP: "1.2.3.1",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8082, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8082,
}},
PortalIP: "1.2.3.2", PortalIP: "1.2.3.2",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8082, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8082,
}},
PortalIP: "None", PortalIP: "None",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8082, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8082,
}},
PortalIP: "", PortalIP: "",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test1"}, ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test1"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8083, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8083,
}},
PortalIP: "1.2.3.3", PortalIP: "1.2.3.3",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "test2"}, ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "test2"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8084, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8084,
}},
PortalIP: "1.2.3.4", PortalIP: "1.2.3.4",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"}, ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8085, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8085,
}},
PortalIP: "1.2.3.5", PortalIP: "1.2.3.5",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"}, ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8085, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8085,
}},
PortalIP: "None", PortalIP: "None",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"}, ObjectMeta: api.ObjectMeta{Name: "test", Namespace: "test2"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8085, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8085,
}},
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "kubernetes"}, ObjectMeta: api.ObjectMeta{Name: "kubernetes", Namespace: "kubernetes"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8086, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8086,
}},
PortalIP: "1.2.3.6", PortalIP: "1.2.3.6",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: "kubernetes"}, ObjectMeta: api.ObjectMeta{Name: "kubernetes-ro", Namespace: "kubernetes"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8087, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8087,
}},
PortalIP: "1.2.3.7", PortalIP: "1.2.3.7",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"}, ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8088, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8088,
}},
PortalIP: "1.2.3.8", PortalIP: "1.2.3.8",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"}, ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8088, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8088,
}},
PortalIP: "None", PortalIP: "None",
}, },
}, },
{ {
ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"}, ObjectMeta: api.ObjectMeta{Name: "not-special", Namespace: "kubernetes"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8088, Ports: []api.ServicePort{{
Protocol: "TCP",
Port: 8088,
}},
PortalIP: "", PortalIP: "",
}, },
}, },

View File

@@ -114,11 +114,10 @@ func (m *Master) createMasterServiceIfNeeded(serviceName string, serviceIP net.I
Labels: map[string]string{"provider": "kubernetes", "component": "apiserver"}, Labels: map[string]string{"provider": "kubernetes", "component": "apiserver"},
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: servicePort, Ports: []api.ServicePort{{Port: servicePort, Protocol: api.ProtocolTCP}},
// maintained by this code, not by the pod selector // maintained by this code, not by the pod selector
Selector: nil, Selector: nil,
PortalIP: serviceIP.String(), PortalIP: serviceIP.String(),
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
}, },
} }

View File

@@ -136,7 +136,10 @@ func TestNewServiceAddedAndNotified(t *testing.T) {
handler := NewServiceHandlerMock() handler := NewServiceHandlerMock()
handler.Wait(1) handler.Wait(1)
config.RegisterHandler(handler) config.RegisterHandler(handler)
serviceUpdate := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) serviceUpdate := CreateServiceUpdate(ADD, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}},
})
channel <- serviceUpdate channel <- serviceUpdate
handler.ValidateServices(t, serviceUpdate.Services) handler.ValidateServices(t, serviceUpdate.Services)
@@ -147,24 +150,35 @@ func TestServiceAddedRemovedSetAndNotified(t *testing.T) {
channel := config.Channel("one") channel := config.Channel("one")
handler := NewServiceHandlerMock() handler := NewServiceHandlerMock()
config.RegisterHandler(handler) config.RegisterHandler(handler)
serviceUpdate := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) serviceUpdate := CreateServiceUpdate(ADD, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}},
})
handler.Wait(1) handler.Wait(1)
channel <- serviceUpdate channel <- serviceUpdate
handler.ValidateServices(t, serviceUpdate.Services) handler.ValidateServices(t, serviceUpdate.Services)
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, Spec: api.ServiceSpec{Port: 20}}) serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 20}}},
})
handler.Wait(1) handler.Wait(1)
channel <- serviceUpdate2 channel <- serviceUpdate2
services := []api.Service{serviceUpdate2.Services[0], serviceUpdate.Services[0]} services := []api.Service{serviceUpdate2.Services[0], serviceUpdate.Services[0]}
handler.ValidateServices(t, services) handler.ValidateServices(t, services)
serviceUpdate3 := CreateServiceUpdate(REMOVE, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}}) serviceUpdate3 := CreateServiceUpdate(REMOVE, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"},
})
handler.Wait(1) handler.Wait(1)
channel <- serviceUpdate3 channel <- serviceUpdate3
services = []api.Service{serviceUpdate2.Services[0]} services = []api.Service{serviceUpdate2.Services[0]}
handler.ValidateServices(t, services) handler.ValidateServices(t, services)
serviceUpdate4 := CreateServiceUpdate(SET, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foobar"}, Spec: api.ServiceSpec{Port: 99}}) serviceUpdate4 := CreateServiceUpdate(SET, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foobar"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 99}}},
})
handler.Wait(1) handler.Wait(1)
channel <- serviceUpdate4 channel <- serviceUpdate4
services = []api.Service{serviceUpdate4.Services[0]} services = []api.Service{serviceUpdate4.Services[0]}
@@ -180,8 +194,14 @@ func TestNewMultipleSourcesServicesAddedAndNotified(t *testing.T) {
} }
handler := NewServiceHandlerMock() handler := NewServiceHandlerMock()
config.RegisterHandler(handler) config.RegisterHandler(handler)
serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, Spec: api.ServiceSpec{Port: 20}}) ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}},
})
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 20}}},
})
handler.Wait(2) handler.Wait(2)
channelOne <- serviceUpdate1 channelOne <- serviceUpdate1
channelTwo <- serviceUpdate2 channelTwo <- serviceUpdate2
@@ -197,8 +217,14 @@ func TestNewMultipleSourcesServicesMultipleHandlersAddedAndNotified(t *testing.T
handler2 := NewServiceHandlerMock() handler2 := NewServiceHandlerMock()
config.RegisterHandler(handler) config.RegisterHandler(handler)
config.RegisterHandler(handler2) config.RegisterHandler(handler2)
serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"}, Spec: api.ServiceSpec{Port: 10}}) serviceUpdate1 := CreateServiceUpdate(ADD, api.Service{
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"}, Spec: api.ServiceSpec{Port: 20}}) ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "foo"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 10}}},
})
serviceUpdate2 := CreateServiceUpdate(ADD, api.Service{
ObjectMeta: api.ObjectMeta{Namespace: "testnamespace", Name: "bar"},
Spec: api.ServiceSpec{Ports: []api.ServicePort{{Protocol: "TCP", Port: 20}}},
})
handler.Wait(2) handler.Wait(2)
handler2.Wait(2) handler2.Wait(2)
channelOne <- serviceUpdate1 channelOne <- serviceUpdate1

View File

@@ -17,6 +17,7 @@ limitations under the License.
package proxy package proxy
import ( import (
"fmt"
"net" "net"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
@@ -27,7 +28,18 @@ import (
type LoadBalancer interface { type LoadBalancer interface {
// NextEndpoint returns the endpoint to handle a request for the given // NextEndpoint returns the endpoint to handle a request for the given
// service-port and source address. // service-port and source address.
NextEndpoint(service types.NamespacedName, port string, srcAddr net.Addr) (string, error) NextEndpoint(service ServicePortName, srcAddr net.Addr) (string, error)
NewService(service types.NamespacedName, port string, sessionAffinityType api.AffinityType, stickyMaxAgeMinutes int) error NewService(service ServicePortName, sessionAffinityType api.AffinityType, stickyMaxAgeMinutes int) error
CleanupStaleStickySessions(service types.NamespacedName, port string) CleanupStaleStickySessions(service ServicePortName)
}
// ServicePortName carries a namespace + name + portname. This is the unique
// identfier for a load-balanced service.
type ServicePortName struct {
types.NamespacedName
Port string
}
func (spn ServicePortName) String() string {
return fmt.Sprintf("%s:%s", spn.NamespacedName.String(), spn.Port)
} }

View File

@@ -35,14 +35,13 @@ import (
) )
type serviceInfo struct { type serviceInfo struct {
portalIP net.IP portalIP net.IP
portalPort int portalPort int
protocol api.Protocol protocol api.Protocol
proxyPort int proxyPort int
socket proxySocket socket proxySocket
timeout time.Duration timeout time.Duration
// TODO: make this an net.IP address publicIPs []string // TODO: make this net.IP
publicIP []string
sessionAffinityType api.AffinityType sessionAffinityType api.AffinityType
stickyMaxAgeMinutes int stickyMaxAgeMinutes int
} }
@@ -59,7 +58,7 @@ type proxySocket interface {
// while sessions are active. // while sessions are active.
Close() error Close() error
// ProxyLoop proxies incoming connections for the specified service to the service endpoints. // ProxyLoop proxies incoming connections for the specified service to the service endpoints.
ProxyLoop(service types.NamespacedName, info *serviceInfo, proxier *Proxier) ProxyLoop(service ServicePortName, info *serviceInfo, proxier *Proxier)
} }
// tcpProxySocket implements proxySocket. Close() is implemented by net.Listener. When Close() is called, // tcpProxySocket implements proxySocket. Close() is implemented by net.Listener. When Close() is called,
@@ -68,10 +67,9 @@ type tcpProxySocket struct {
net.Listener net.Listener
} }
func tryConnect(service types.NamespacedName, srcAddr net.Addr, protocol string, proxier *Proxier) (out net.Conn, err error) { func tryConnect(service ServicePortName, srcAddr net.Addr, protocol string, proxier *Proxier) (out net.Conn, err error) {
for _, retryTimeout := range endpointDialTimeout { for _, retryTimeout := range endpointDialTimeout {
// TODO: support multiple service ports endpoint, err := proxier.loadBalancer.NextEndpoint(service, srcAddr)
endpoint, err := proxier.loadBalancer.NextEndpoint(service, "", srcAddr)
if err != nil { if err != nil {
glog.Errorf("Couldn't find an endpoint for %s: %v", service, err) glog.Errorf("Couldn't find an endpoint for %s: %v", service, err)
return nil, err return nil, err
@@ -89,7 +87,7 @@ func tryConnect(service types.NamespacedName, srcAddr net.Addr, protocol string,
return nil, fmt.Errorf("failed to connect to an endpoint.") return nil, fmt.Errorf("failed to connect to an endpoint.")
} }
func (tcp *tcpProxySocket) ProxyLoop(service types.NamespacedName, myInfo *serviceInfo, proxier *Proxier) { func (tcp *tcpProxySocket) ProxyLoop(service ServicePortName, myInfo *serviceInfo, proxier *Proxier) {
for { for {
if info, exists := proxier.getServiceInfo(service); !exists || info != myInfo { if info, exists := proxier.getServiceInfo(service); !exists || info != myInfo {
// The service port was closed or replaced. // The service port was closed or replaced.
@@ -164,7 +162,7 @@ func newClientCache() *clientCache {
return &clientCache{clients: map[string]net.Conn{}} return &clientCache{clients: map[string]net.Conn{}}
} }
func (udp *udpProxySocket) ProxyLoop(service types.NamespacedName, myInfo *serviceInfo, proxier *Proxier) { func (udp *udpProxySocket) ProxyLoop(service ServicePortName, myInfo *serviceInfo, proxier *Proxier) {
activeClients := newClientCache() activeClients := newClientCache()
var buffer [4096]byte // 4KiB should be enough for most whole-packets var buffer [4096]byte // 4KiB should be enough for most whole-packets
for { for {
@@ -209,7 +207,7 @@ func (udp *udpProxySocket) ProxyLoop(service types.NamespacedName, myInfo *servi
} }
} }
func (udp *udpProxySocket) getBackendConn(activeClients *clientCache, cliAddr net.Addr, proxier *Proxier, service types.NamespacedName, timeout time.Duration) (net.Conn, error) { func (udp *udpProxySocket) getBackendConn(activeClients *clientCache, cliAddr net.Addr, proxier *Proxier, service ServicePortName, timeout time.Duration) (net.Conn, error) {
activeClients.mu.Lock() activeClients.mu.Lock()
defer activeClients.mu.Unlock() defer activeClients.mu.Unlock()
@@ -305,7 +303,7 @@ func newProxySocket(protocol api.Protocol, ip net.IP, port int) (proxySocket, er
type Proxier struct { type Proxier struct {
loadBalancer LoadBalancer loadBalancer LoadBalancer
mu sync.Mutex // protects serviceMap mu sync.Mutex // protects serviceMap
serviceMap map[types.NamespacedName]*serviceInfo serviceMap map[ServicePortName]*serviceInfo
numProxyLoops int32 // use atomic ops to access this; mostly for testing numProxyLoops int32 // use atomic ops to access this; mostly for testing
listenIP net.IP listenIP net.IP
iptables iptables.Interface iptables iptables.Interface
@@ -347,7 +345,7 @@ func CreateProxier(loadBalancer LoadBalancer, listenIP net.IP, iptables iptables
} }
return &Proxier{ return &Proxier{
loadBalancer: loadBalancer, loadBalancer: loadBalancer,
serviceMap: make(map[types.NamespacedName]*serviceInfo), serviceMap: make(map[ServicePortName]*serviceInfo),
listenIP: listenIP, listenIP: listenIP,
iptables: iptables, iptables: iptables,
hostIP: hostIP, hostIP: hostIP,
@@ -387,35 +385,32 @@ func (proxier *Proxier) ensurePortals() {
// clean up any stale sticky session records in the hash map. // clean up any stale sticky session records in the hash map.
func (proxier *Proxier) cleanupStaleStickySessions() { func (proxier *Proxier) cleanupStaleStickySessions() {
for name, info := range proxier.serviceMap { for name := range proxier.serviceMap {
if info.sessionAffinityType != api.AffinityTypeNone { proxier.loadBalancer.CleanupStaleStickySessions(name)
// TODO: support multiple service ports
proxier.loadBalancer.CleanupStaleStickySessions(name, "")
}
} }
} }
// This assumes proxier.mu is not locked. // This assumes proxier.mu is not locked.
func (proxier *Proxier) stopProxy(service types.NamespacedName, info *serviceInfo) error { func (proxier *Proxier) stopProxy(service ServicePortName, info *serviceInfo) error {
proxier.mu.Lock() proxier.mu.Lock()
defer proxier.mu.Unlock() defer proxier.mu.Unlock()
return proxier.stopProxyInternal(service, info) return proxier.stopProxyInternal(service, info)
} }
// This assumes proxier.mu is locked. // This assumes proxier.mu is locked.
func (proxier *Proxier) stopProxyInternal(service types.NamespacedName, info *serviceInfo) error { func (proxier *Proxier) stopProxyInternal(service ServicePortName, info *serviceInfo) error {
delete(proxier.serviceMap, service) delete(proxier.serviceMap, service)
return info.socket.Close() return info.socket.Close()
} }
func (proxier *Proxier) getServiceInfo(service types.NamespacedName) (*serviceInfo, bool) { func (proxier *Proxier) getServiceInfo(service ServicePortName) (*serviceInfo, bool) {
proxier.mu.Lock() proxier.mu.Lock()
defer proxier.mu.Unlock() defer proxier.mu.Unlock()
info, ok := proxier.serviceMap[service] info, ok := proxier.serviceMap[service]
return info, ok return info, ok
} }
func (proxier *Proxier) setServiceInfo(service types.NamespacedName, info *serviceInfo) { func (proxier *Proxier) setServiceInfo(service ServicePortName, info *serviceInfo) {
proxier.mu.Lock() proxier.mu.Lock()
defer proxier.mu.Unlock() defer proxier.mu.Unlock()
proxier.serviceMap[service] = info proxier.serviceMap[service] = info
@@ -424,7 +419,7 @@ func (proxier *Proxier) setServiceInfo(service types.NamespacedName, info *servi
// addServiceOnPort starts listening for a new service, returning the serviceInfo. // addServiceOnPort starts listening for a new service, returning the serviceInfo.
// Pass proxyPort=0 to allocate a random port. The timeout only applies to UDP // Pass proxyPort=0 to allocate a random port. The timeout only applies to UDP
// connections, for now. // connections, for now.
func (proxier *Proxier) addServiceOnPort(service types.NamespacedName, protocol api.Protocol, proxyPort int, timeout time.Duration) (*serviceInfo, error) { func (proxier *Proxier) addServiceOnPort(service ServicePortName, protocol api.Protocol, proxyPort int, timeout time.Duration) (*serviceInfo, error) {
sock, err := newProxySocket(protocol, proxier.listenIP, proxyPort) sock, err := newProxySocket(protocol, proxier.listenIP, proxyPort)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -444,13 +439,13 @@ func (proxier *Proxier) addServiceOnPort(service types.NamespacedName, protocol
protocol: protocol, protocol: protocol,
socket: sock, socket: sock,
timeout: timeout, timeout: timeout,
sessionAffinityType: api.AffinityTypeNone, sessionAffinityType: api.AffinityTypeNone, // default
stickyMaxAgeMinutes: 180, stickyMaxAgeMinutes: 180, // TODO: paramaterize this in the API.
} }
proxier.setServiceInfo(service, si) proxier.setServiceInfo(service, si)
glog.V(1).Infof("Proxying for service %q on %s port %d", service, protocol, portNum) glog.V(1).Infof("Proxying for service %q on %s port %d", service, protocol, portNum)
go func(service types.NamespacedName, proxier *Proxier) { go func(service ServicePortName, proxier *Proxier) {
defer util.HandleCrash() defer util.HandleCrash()
atomic.AddInt32(&proxier.numProxyLoops, 1) atomic.AddInt32(&proxier.numProxyLoops, 1)
sock.ProxyLoop(service, si, proxier) sock.ProxyLoop(service, si, proxier)
@@ -468,51 +463,57 @@ const udpIdleTimeout = 1 * time.Minute
// shutdown if missing from the update set. // shutdown if missing from the update set.
func (proxier *Proxier) OnUpdate(services []api.Service) { func (proxier *Proxier) OnUpdate(services []api.Service) {
glog.V(4).Infof("Received update notice: %+v", services) glog.V(4).Infof("Received update notice: %+v", services)
activeServices := make(map[types.NamespacedName]bool) // use a map as a set activeServices := make(map[ServicePortName]bool) // use a map as a set
for _, service := range services { for i := range services {
// if PortalIP is "None" or empty, skip proxying service := &services[i]
if !api.IsServiceIPSet(&service) {
continue
}
serviceName := types.NamespacedName{service.Namespace, service.Name}
activeServices[serviceName] = true
info, exists := proxier.getServiceInfo(serviceName)
serviceIP := net.ParseIP(service.Spec.PortalIP)
// TODO: check health of the socket? What if ProxyLoop exited?
if exists && info.portalPort == service.Spec.Port && info.portalIP.Equal(serviceIP) && ipsEqual(service.Spec.PublicIPs, info.publicIP) {
continue
}
if exists {
glog.V(4).Infof("Something changed for service %q: stopping it", serviceName.String())
err := proxier.closePortal(serviceName, info)
if err != nil {
glog.Errorf("Failed to close portal for %q: %v", serviceName, err)
}
err = proxier.stopProxy(serviceName, info)
if err != nil {
glog.Errorf("Failed to stop service %q: %v", serviceName, err)
}
}
glog.V(1).Infof("Adding new service %q at %s:%d/%s", serviceName, serviceIP, service.Spec.Port, service.Spec.Protocol)
info, err := proxier.addServiceOnPort(serviceName, service.Spec.Protocol, 0, udpIdleTimeout)
if err != nil {
glog.Errorf("Failed to start proxy for %q: %v", serviceName, err)
continue
}
info.portalIP = serviceIP
info.portalPort = service.Spec.Port
info.publicIP = service.Spec.PublicIPs
info.sessionAffinityType = service.Spec.SessionAffinity
// TODO: paramaterize this in the types api file as an attribute of sticky session. For now it's hardcoded to 3 hours.
info.stickyMaxAgeMinutes = 180
glog.V(4).Infof("info: %+v", info)
err = proxier.openPortal(serviceName, info) // if PortalIP is "None" or empty, skip proxying
if err != nil { if !api.IsServiceIPSet(service) {
glog.Errorf("Failed to open portal for %q: %v", serviceName, err) glog.V(3).Infof("Skipping service %s due to portal IP = %q", types.NamespacedName{service.Namespace, service.Name}, service.Spec.PortalIP)
continue
}
for i := range service.Spec.Ports {
servicePort := &service.Spec.Ports[i]
serviceName := ServicePortName{types.NamespacedName{service.Namespace, service.Name}, servicePort.Name}
activeServices[serviceName] = true
serviceIP := net.ParseIP(service.Spec.PortalIP)
info, exists := proxier.getServiceInfo(serviceName)
// TODO: check health of the socket? What if ProxyLoop exited?
if exists && sameConfig(info, service, servicePort) {
// Nothing changed.
continue
}
if exists {
glog.V(4).Infof("Something changed for service %q: stopping it", serviceName)
err := proxier.closePortal(serviceName, info)
if err != nil {
glog.Errorf("Failed to close portal for %q: %v", serviceName, err)
}
err = proxier.stopProxy(serviceName, info)
if err != nil {
glog.Errorf("Failed to stop service %q: %v", serviceName, err)
}
}
glog.V(1).Infof("Adding new service %q at %s:%d/%s", serviceName, serviceIP, servicePort.Port, servicePort.Protocol)
info, err := proxier.addServiceOnPort(serviceName, servicePort.Protocol, 0, udpIdleTimeout)
if err != nil {
glog.Errorf("Failed to start proxy for %q: %v", serviceName, err)
continue
}
info.portalIP = serviceIP
info.portalPort = servicePort.Port
info.publicIPs = service.Spec.PublicIPs
info.sessionAffinityType = service.Spec.SessionAffinity
glog.V(4).Infof("info: %+v", info)
err = proxier.openPortal(serviceName, info)
if err != nil {
glog.Errorf("Failed to open portal for %q: %v", serviceName, err)
}
proxier.loadBalancer.NewService(serviceName, info.sessionAffinityType, info.stickyMaxAgeMinutes)
} }
// TODO: support multiple service ports
proxier.loadBalancer.NewService(serviceName, "", info.sessionAffinityType, info.stickyMaxAgeMinutes)
} }
proxier.mu.Lock() proxier.mu.Lock()
defer proxier.mu.Unlock() defer proxier.mu.Unlock()
@@ -531,6 +532,22 @@ func (proxier *Proxier) OnUpdate(services []api.Service) {
} }
} }
func sameConfig(info *serviceInfo, service *api.Service, port *api.ServicePort) bool {
if info.protocol != port.Protocol || info.portalPort != port.Port {
return false
}
if !info.portalIP.Equal(net.ParseIP(service.Spec.PortalIP)) {
return false
}
if !ipsEqual(info.publicIPs, service.Spec.PublicIPs) {
return false
}
if info.sessionAffinityType != service.Spec.SessionAffinity {
return false
}
return true
}
func ipsEqual(lhs, rhs []string) bool { func ipsEqual(lhs, rhs []string) bool {
if len(lhs) != len(rhs) { if len(lhs) != len(rhs) {
return false return false
@@ -543,12 +560,12 @@ func ipsEqual(lhs, rhs []string) bool {
return true return true
} }
func (proxier *Proxier) openPortal(service types.NamespacedName, info *serviceInfo) error { func (proxier *Proxier) openPortal(service ServicePortName, info *serviceInfo) error {
err := proxier.openOnePortal(info.portalIP, info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service) err := proxier.openOnePortal(info.portalIP, info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)
if err != nil { if err != nil {
return err return err
} }
for _, publicIP := range info.publicIP { for _, publicIP := range info.publicIPs {
err = proxier.openOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service) err = proxier.openOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)
if err != nil { if err != nil {
return err return err
@@ -557,7 +574,7 @@ func (proxier *Proxier) openPortal(service types.NamespacedName, info *serviceIn
return nil return nil
} }
func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name types.NamespacedName) error { func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) error {
// Handle traffic from containers. // Handle traffic from containers.
args := proxier.iptablesContainerPortalArgs(portalIP, portalPort, protocol, proxyIP, proxyPort, name) args := proxier.iptablesContainerPortalArgs(portalIP, portalPort, protocol, proxyIP, proxyPort, name)
existed, err := proxier.iptables.EnsureRule(iptables.TableNAT, iptablesContainerPortalChain, args...) existed, err := proxier.iptables.EnsureRule(iptables.TableNAT, iptablesContainerPortalChain, args...)
@@ -582,10 +599,10 @@ func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol
return nil return nil
} }
func (proxier *Proxier) closePortal(service types.NamespacedName, info *serviceInfo) error { func (proxier *Proxier) closePortal(service ServicePortName, info *serviceInfo) error {
// Collect errors and report them all at the end. // Collect errors and report them all at the end.
el := proxier.closeOnePortal(info.portalIP, info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service) el := proxier.closeOnePortal(info.portalIP, info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)
for _, publicIP := range info.publicIP { for _, publicIP := range info.publicIPs {
el = append(el, proxier.closeOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)...) el = append(el, proxier.closeOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)...)
} }
if len(el) == 0 { if len(el) == 0 {
@@ -596,7 +613,7 @@ func (proxier *Proxier) closePortal(service types.NamespacedName, info *serviceI
return errors.NewAggregate(el) return errors.NewAggregate(el)
} }
func (proxier *Proxier) closeOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name types.NamespacedName) []error { func (proxier *Proxier) closeOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) []error {
el := []error{} el := []error{}
// Handle traffic from containers. // Handle traffic from containers.
@@ -675,7 +692,7 @@ var zeroIPv6 = net.ParseIP("::0")
var localhostIPv6 = net.ParseIP("::1") var localhostIPv6 = net.ParseIP("::1")
// Build a slice of iptables args that are common to from-container and from-host portal rules. // Build a slice of iptables args that are common to from-container and from-host portal rules.
func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, service types.NamespacedName) []string { func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, service ServicePortName) []string {
// This list needs to include all fields as they are eventually spit out // This list needs to include all fields as they are eventually spit out
// by iptables-save. This is because some systems do not support the // by iptables-save. This is because some systems do not support the
// 'iptables -C' arg, and so fall back on parsing iptables-save output. // 'iptables -C' arg, and so fall back on parsing iptables-save output.
@@ -696,7 +713,7 @@ func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol
} }
// Build a slice of iptables args for a from-container portal rule. // Build a slice of iptables args for a from-container portal rule.
func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service types.NamespacedName) []string { func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service ServicePortName) []string {
args := iptablesCommonPortalArgs(destIP, destPort, protocol, service) args := iptablesCommonPortalArgs(destIP, destPort, protocol, service)
// This is tricky. // This is tricky.
@@ -743,7 +760,7 @@ func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int,
} }
// Build a slice of iptables args for a from-host portal rule. // Build a slice of iptables args for a from-host portal rule.
func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service types.NamespacedName) []string { func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service ServicePortName) []string {
args := iptablesCommonPortalArgs(destIP, destPort, protocol, service) args := iptablesCommonPortalArgs(destIP, destPort, protocol, service)
// This is tricky. // This is tricky.

View File

@@ -195,13 +195,13 @@ func waitForNumProxyLoops(t *testing.T, p *Proxier, want int32) {
func TestTCPProxy(t *testing.T) { func TestTCPProxy(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -219,13 +219,13 @@ func TestTCPProxy(t *testing.T) {
func TestUDPProxy(t *testing.T) { func TestUDPProxy(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: udpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}},
}}, }},
}, },
}) })
@@ -241,8 +241,88 @@ func TestUDPProxy(t *testing.T) {
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
} }
func TestMultiPortProxy(t *testing.T) {
lb := NewLoadBalancerRR()
serviceP := ServicePortName{types.NamespacedName{"testnamespace", "echo-p"}, "p"}
serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "echo-q"}, "q"}
lb.OnUpdate([]api.Endpoints{{
ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace},
Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Name: "p", Protocol: "TCP", Port: tcpServerPort}},
}},
}, {
ObjectMeta: api.ObjectMeta{Name: serviceQ.Name, Namespace: serviceQ.Namespace},
Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Name: "q", Protocol: "UDP", Port: udpServerPort}},
}},
}})
p := CreateProxier(lb, net.ParseIP("0.0.0.0"), &fakeIptables{}, net.ParseIP("127.0.0.1"))
waitForNumProxyLoops(t, p, 0)
svcInfoP, err := p.addServiceOnPort(serviceP, "TCP", 0, time.Second)
if err != nil {
t.Fatalf("error adding new service: %#v", err)
}
testEchoTCP(t, "127.0.0.1", svcInfoP.proxyPort)
waitForNumProxyLoops(t, p, 1)
svcInfoQ, err := p.addServiceOnPort(serviceQ, "UDP", 0, time.Second)
if err != nil {
t.Fatalf("error adding new service: %#v", err)
}
testEchoUDP(t, "127.0.0.1", svcInfoQ.proxyPort)
waitForNumProxyLoops(t, p, 2)
}
func TestMultiPortOnUpdate(t *testing.T) {
lb := NewLoadBalancerRR()
serviceP := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "q"}
serviceX := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "x"}
p := CreateProxier(lb, net.ParseIP("0.0.0.0"), &fakeIptables{}, net.ParseIP("127.0.0.1"))
waitForNumProxyLoops(t, p, 0)
p.OnUpdate([]api.Service{{
ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace},
Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{
Name: "p",
Port: 80,
Protocol: "TCP",
}, {
Name: "q",
Port: 81,
Protocol: "UDP",
}}},
}})
waitForNumProxyLoops(t, p, 2)
svcInfo, exists := p.getServiceInfo(serviceP)
if !exists {
t.Fatalf("can't find serviceInfo for %s", serviceP)
}
if svcInfo.portalIP.String() != "1.2.3.4" || svcInfo.portalPort != 80 || svcInfo.protocol != "TCP" {
t.Errorf("unexpected serviceInfo for %s: %#v", serviceP, svcInfo)
}
svcInfo, exists = p.getServiceInfo(serviceQ)
if !exists {
t.Fatalf("can't find serviceInfo for %s", serviceQ)
}
if svcInfo.portalIP.String() != "1.2.3.4" || svcInfo.portalPort != 81 || svcInfo.protocol != "UDP" {
t.Errorf("unexpected serviceInfo for %s: %#v", serviceQ, svcInfo)
}
svcInfo, exists = p.getServiceInfo(serviceX)
if exists {
t.Fatalf("found unwanted serviceInfo for %s: %#v", serviceX, svcInfo)
}
}
// Helper: Stops the proxy for the named service. // Helper: Stops the proxy for the named service.
func stopProxyByName(proxier *Proxier, service types.NamespacedName) error { func stopProxyByName(proxier *Proxier, service ServicePortName) error {
info, found := proxier.getServiceInfo(service) info, found := proxier.getServiceInfo(service)
if !found { if !found {
return fmt.Errorf("unknown service: %s", service) return fmt.Errorf("unknown service: %s", service)
@@ -252,13 +332,13 @@ func stopProxyByName(proxier *Proxier, service types.NamespacedName) error {
func TestTCPProxyStop(t *testing.T) { func TestTCPProxyStop(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -287,13 +367,13 @@ func TestTCPProxyStop(t *testing.T) {
func TestUDPProxyStop(t *testing.T) { func TestUDPProxyStop(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: udpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}},
}}, }},
}, },
}) })
@@ -322,13 +402,13 @@ func TestUDPProxyStop(t *testing.T) {
func TestTCPProxyUpdateDelete(t *testing.T) { func TestTCPProxyUpdateDelete(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -356,13 +436,13 @@ func TestTCPProxyUpdateDelete(t *testing.T) {
func TestUDPProxyUpdateDelete(t *testing.T) { func TestUDPProxyUpdateDelete(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name}, ObjectMeta: api.ObjectMeta{Namespace: service.Namespace, Name: service.Name},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: udpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}},
}}, }},
}, },
}) })
@@ -390,13 +470,13 @@ func TestUDPProxyUpdateDelete(t *testing.T) {
func TestTCPProxyUpdateDeleteUpdate(t *testing.T) { func TestTCPProxyUpdateDeleteUpdate(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -420,9 +500,15 @@ func TestTCPProxyUpdateDeleteUpdate(t *testing.T) {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }
waitForNumProxyLoops(t, p, 0) waitForNumProxyLoops(t, p, 0)
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, p.OnUpdate([]api.Service{{
}) ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{
Name: "p",
Port: svcInfo.proxyPort,
Protocol: "TCP",
}}},
}})
svcInfo, exists := p.getServiceInfo(service) svcInfo, exists := p.getServiceInfo(service)
if !exists { if !exists {
t.Fatalf("can't find serviceInfo for %s", service) t.Fatalf("can't find serviceInfo for %s", service)
@@ -433,13 +519,13 @@ func TestTCPProxyUpdateDeleteUpdate(t *testing.T) {
func TestUDPProxyUpdateDeleteUpdate(t *testing.T) { func TestUDPProxyUpdateDeleteUpdate(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: udpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}},
}}, }},
}, },
}) })
@@ -463,9 +549,15 @@ func TestUDPProxyUpdateDeleteUpdate(t *testing.T) {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }
waitForNumProxyLoops(t, p, 0) waitForNumProxyLoops(t, p, 0)
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "UDP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, p.OnUpdate([]api.Service{{
}) ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{
Name: "p",
Port: svcInfo.proxyPort,
Protocol: "UDP",
}}},
}})
svcInfo, exists := p.getServiceInfo(service) svcInfo, exists := p.getServiceInfo(service)
if !exists { if !exists {
t.Fatalf("can't find serviceInfo") t.Fatalf("can't find serviceInfo")
@@ -476,13 +568,13 @@ func TestUDPProxyUpdateDeleteUpdate(t *testing.T) {
func TestTCPProxyUpdatePort(t *testing.T) { func TestTCPProxyUpdatePort(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -497,9 +589,14 @@ func TestTCPProxyUpdatePort(t *testing.T) {
testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort) testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort)
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
}) Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{
Name: "p",
Port: 99,
Protocol: "TCP",
}}},
}})
// Wait for the socket to actually get free. // Wait for the socket to actually get free.
if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil { if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
@@ -516,13 +613,13 @@ func TestTCPProxyUpdatePort(t *testing.T) {
func TestUDPProxyUpdatePort(t *testing.T) { func TestUDPProxyUpdatePort(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: udpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: udpServerPort}},
}}, }},
}, },
}) })
@@ -536,9 +633,14 @@ func TestUDPProxyUpdatePort(t *testing.T) {
} }
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: 99, Protocol: "UDP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
}) Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{
Name: "p",
Port: 99,
Protocol: "UDP",
}}},
}})
// Wait for the socket to actually get free. // Wait for the socket to actually get free.
if err := waitForClosedPortUDP(p, svcInfo.proxyPort); err != nil { if err := waitForClosedPortUDP(p, svcInfo.proxyPort); err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
@@ -553,13 +655,13 @@ func TestUDPProxyUpdatePort(t *testing.T) {
func TestProxyUpdatePublicIPs(t *testing.T) { func TestProxyUpdatePublicIPs(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -574,9 +676,18 @@ func TestProxyUpdatePublicIPs(t *testing.T) {
testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort) testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort)
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.portalPort, Protocol: "TCP", PortalIP: svcInfo.portalIP.String(), PublicIPs: []string{"4.3.2.1"}}, Status: api.ServiceStatus{}}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
}) Spec: api.ServiceSpec{
Ports: []api.ServicePort{{
Name: "p",
Port: svcInfo.portalPort,
Protocol: "TCP",
}},
PortalIP: svcInfo.portalIP.String(),
PublicIPs: []string{"4.3.2.1"},
},
}})
// Wait for the socket to actually get free. // Wait for the socket to actually get free.
if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil { if err := waitForClosedPortTCP(p, svcInfo.proxyPort); err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
@@ -593,13 +704,13 @@ func TestProxyUpdatePublicIPs(t *testing.T) {
func TestProxyUpdatePortal(t *testing.T) { func TestProxyUpdatePortal(t *testing.T) {
lb := NewLoadBalancerRR() lb := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "echo") service := ServicePortName{types.NamespacedName{"testnamespace", "echo"}, "p"}
lb.OnUpdate([]api.Endpoints{ lb.OnUpdate([]api.Endpoints{
{ {
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{ Subsets: []api.EndpointSubset{{
Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}}, Addresses: []api.EndpointAddress{{IP: "127.0.0.1"}},
Ports: []api.EndpointPort{{Port: tcpServerPort}}, Ports: []api.EndpointPort{{Name: "p", Port: tcpServerPort}},
}}, }},
}, },
}) })
@@ -614,33 +725,40 @@ func TestProxyUpdatePortal(t *testing.T) {
testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort) testEchoTCP(t, "127.0.0.1", svcInfo.proxyPort)
waitForNumProxyLoops(t, p, 1) waitForNumProxyLoops(t, p, 1)
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP"}, Status: api.ServiceStatus{}}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
}) Spec: api.ServiceSpec{PortalIP: "", Ports: []api.ServicePort{{
Name: "p",
Port: svcInfo.proxyPort,
Protocol: "TCP",
}}},
}})
_, exists := p.getServiceInfo(service) _, exists := p.getServiceInfo(service)
if exists {
t.Fatalf("service without portalIP should not be included in the proxy")
}
p.OnUpdate([]api.Service{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: ""}, Status: api.ServiceStatus{}},
})
_, exists = p.getServiceInfo(service)
if exists { if exists {
t.Fatalf("service with empty portalIP should not be included in the proxy") t.Fatalf("service with empty portalIP should not be included in the proxy")
} }
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "None"}, Status: api.ServiceStatus{}}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
}) Spec: api.ServiceSpec{PortalIP: "None", Ports: []api.ServicePort{{
Name: "p",
Port: svcInfo.proxyPort,
Protocol: "TCP",
}}},
}})
_, exists = p.getServiceInfo(service) _, exists = p.getServiceInfo(service)
if exists { if exists {
t.Fatalf("service with 'None' as portalIP should not be included in the proxy") t.Fatalf("service with 'None' as portalIP should not be included in the proxy")
} }
p.OnUpdate([]api.Service{ p.OnUpdate([]api.Service{{
{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Spec: api.ServiceSpec{Port: svcInfo.proxyPort, Protocol: "TCP", PortalIP: "1.2.3.4"}, Status: api.ServiceStatus{}}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
}) Spec: api.ServiceSpec{PortalIP: "1.2.3.4", Ports: []api.ServicePort{{
Name: "p",
Port: svcInfo.proxyPort,
Protocol: "TCP",
}}},
}})
svcInfo, exists = p.getServiceInfo(service) svcInfo, exists = p.getServiceInfo(service)
if !exists { if !exists {
t.Fatalf("service with portalIP set not found in the proxy") t.Fatalf("service with portalIP set not found in the proxy")

View File

@@ -50,20 +50,10 @@ type affinityPolicy struct {
ttlMinutes int ttlMinutes int
} }
// servicePort is the type that the balancer uses to key stored state.
type servicePort struct {
types.NamespacedName
port string
}
func (sp servicePort) String() string {
return fmt.Sprintf("%s:%s", sp.NamespacedName, sp.port)
}
// LoadBalancerRR is a round-robin load balancer. // LoadBalancerRR is a round-robin load balancer.
type LoadBalancerRR struct { type LoadBalancerRR struct {
lock sync.RWMutex lock sync.RWMutex
services map[servicePort]*balancerState services map[ServicePortName]*balancerState
} }
// Ensure this implements LoadBalancer. // Ensure this implements LoadBalancer.
@@ -86,13 +76,11 @@ func newAffinityPolicy(affinityType api.AffinityType, ttlMinutes int) *affinityP
// NewLoadBalancerRR returns a new LoadBalancerRR. // NewLoadBalancerRR returns a new LoadBalancerRR.
func NewLoadBalancerRR() *LoadBalancerRR { func NewLoadBalancerRR() *LoadBalancerRR {
return &LoadBalancerRR{ return &LoadBalancerRR{
services: map[servicePort]*balancerState{}, services: map[ServicePortName]*balancerState{},
} }
} }
func (lb *LoadBalancerRR) NewService(service types.NamespacedName, port string, affinityType api.AffinityType, ttlMinutes int) error { func (lb *LoadBalancerRR) NewService(svcPort ServicePortName, affinityType api.AffinityType, ttlMinutes int) error {
svcPort := servicePort{service, port}
lb.lock.Lock() lb.lock.Lock()
defer lb.lock.Unlock() defer lb.lock.Unlock()
lb.newServiceInternal(svcPort, affinityType, ttlMinutes) lb.newServiceInternal(svcPort, affinityType, ttlMinutes)
@@ -100,7 +88,7 @@ func (lb *LoadBalancerRR) NewService(service types.NamespacedName, port string,
} }
// This assumes that lb.lock is already held. // This assumes that lb.lock is already held.
func (lb *LoadBalancerRR) newServiceInternal(svcPort servicePort, affinityType api.AffinityType, ttlMinutes int) *balancerState { func (lb *LoadBalancerRR) newServiceInternal(svcPort ServicePortName, affinityType api.AffinityType, ttlMinutes int) *balancerState {
if ttlMinutes == 0 { if ttlMinutes == 0 {
ttlMinutes = 180 //default to 3 hours if not specified. Should 0 be unlimeted instead???? ttlMinutes = 180 //default to 3 hours if not specified. Should 0 be unlimeted instead????
} }
@@ -123,9 +111,7 @@ func isSessionAffinity(affinity *affinityPolicy) bool {
// NextEndpoint returns a service endpoint. // NextEndpoint returns a service endpoint.
// The service endpoint is chosen using the round-robin algorithm. // The service endpoint is chosen using the round-robin algorithm.
func (lb *LoadBalancerRR) NextEndpoint(service types.NamespacedName, port string, srcAddr net.Addr) (string, error) { func (lb *LoadBalancerRR) NextEndpoint(svcPort ServicePortName, srcAddr net.Addr) (string, error) {
svcPort := servicePort{service, port}
// Coarse locking is simple. We can get more fine-grained if/when we // Coarse locking is simple. We can get more fine-grained if/when we
// can prove it matters. // can prove it matters.
lb.lock.Lock() lb.lock.Lock()
@@ -202,7 +188,7 @@ func flattenValidEndpoints(endpoints []hostPortPair) []string {
} }
// Remove any session affinity records associated to a particular endpoint (for example when a pod goes down). // Remove any session affinity records associated to a particular endpoint (for example when a pod goes down).
func removeSessionAffinityByEndpoint(state *balancerState, svcPort servicePort, endpoint string) { func removeSessionAffinityByEndpoint(state *balancerState, svcPort ServicePortName, endpoint string) {
for _, affinity := range state.affinity.affinityMap { for _, affinity := range state.affinity.affinityMap {
if affinity.endpoint == endpoint { if affinity.endpoint == endpoint {
glog.V(4).Infof("Removing client: %s from affinityMap for service %q", affinity.endpoint, svcPort) glog.V(4).Infof("Removing client: %s from affinityMap for service %q", affinity.endpoint, svcPort)
@@ -214,7 +200,7 @@ func removeSessionAffinityByEndpoint(state *balancerState, svcPort servicePort,
// Loop through the valid endpoints and then the endpoints associated with the Load Balancer. // Loop through the valid endpoints and then the endpoints associated with the Load Balancer.
// Then remove any session affinity records that are not in both lists. // Then remove any session affinity records that are not in both lists.
// This assumes the lb.lock is held. // This assumes the lb.lock is held.
func (lb *LoadBalancerRR) updateAffinityMap(svcPort servicePort, newEndpoints []string) { func (lb *LoadBalancerRR) updateAffinityMap(svcPort ServicePortName, newEndpoints []string) {
allEndpoints := map[string]int{} allEndpoints := map[string]int{}
for _, newEndpoint := range newEndpoints { for _, newEndpoint := range newEndpoints {
allEndpoints[newEndpoint] = 1 allEndpoints[newEndpoint] = 1
@@ -238,7 +224,7 @@ func (lb *LoadBalancerRR) updateAffinityMap(svcPort servicePort, newEndpoints []
// Registered endpoints are updated if found in the update set or // Registered endpoints are updated if found in the update set or
// unregistered if missing from the update set. // unregistered if missing from the update set.
func (lb *LoadBalancerRR) OnUpdate(allEndpoints []api.Endpoints) { func (lb *LoadBalancerRR) OnUpdate(allEndpoints []api.Endpoints) {
registeredEndpoints := make(map[servicePort]bool) registeredEndpoints := make(map[ServicePortName]bool)
lb.lock.Lock() lb.lock.Lock()
defer lb.lock.Unlock() defer lb.lock.Unlock()
@@ -262,7 +248,7 @@ func (lb *LoadBalancerRR) OnUpdate(allEndpoints []api.Endpoints) {
} }
for portname := range portsToEndpoints { for portname := range portsToEndpoints {
svcPort := servicePort{types.NamespacedName{svcEndpoints.Namespace, svcEndpoints.Name}, portname} svcPort := ServicePortName{types.NamespacedName{svcEndpoints.Namespace, svcEndpoints.Name}, portname}
state, exists := lb.services[svcPort] state, exists := lb.services[svcPort]
curEndpoints := []string{} curEndpoints := []string{}
if state != nil { if state != nil {
@@ -305,15 +291,12 @@ func slicesEquiv(lhs, rhs []string) bool {
return false return false
} }
func (lb *LoadBalancerRR) CleanupStaleStickySessions(service types.NamespacedName, port string) { func (lb *LoadBalancerRR) CleanupStaleStickySessions(svcPort ServicePortName) {
svcPort := servicePort{service, port}
lb.lock.Lock() lb.lock.Lock()
defer lb.lock.Unlock() defer lb.lock.Unlock()
state, exists := lb.services[svcPort] state, exists := lb.services[svcPort]
if !exists { if !exists {
glog.Warning("CleanupStaleStickySessions called for non-existent balancer key %q", svcPort)
return return
} }
for ip, affinity := range state.affinity.affinityMap { for ip, affinity := range state.affinity.affinityMap {

View File

@@ -67,8 +67,8 @@ func TestLoadBalanceFailsWithNoEndpoints(t *testing.T) {
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
var endpoints []api.Endpoints var endpoints []api.Endpoints
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "does-not-exist"}
endpoint, err := loadBalancer.NextEndpoint(service, "does-not-exist", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil { if err == nil {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
@@ -77,20 +77,20 @@ func TestLoadBalanceFailsWithNoEndpoints(t *testing.T) {
} }
} }
func expectEndpoint(t *testing.T, loadBalancer *LoadBalancerRR, service types.NamespacedName, port string, expected string, netaddr net.Addr) { func expectEndpoint(t *testing.T, loadBalancer *LoadBalancerRR, service ServicePortName, expected string, netaddr net.Addr) {
endpoint, err := loadBalancer.NextEndpoint(service, port, netaddr) endpoint, err := loadBalancer.NextEndpoint(service, netaddr)
if err != nil { if err != nil {
t.Errorf("Didn't find a service for %s:%s, expected %s, failed with: %v", service, port, expected, err) t.Errorf("Didn't find a service for %s, expected %s, failed with: %v", service, expected, err)
} }
if endpoint != expected { if endpoint != expected {
t.Errorf("Didn't get expected endpoint for service %s:%s client %v, expected %s, got: %s", service, port, netaddr, expected, endpoint) t.Errorf("Didn't get expected endpoint for service %s client %v, expected %s, got: %s", service, netaddr, expected, endpoint)
} }
} }
func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) { func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) {
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"}
endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
@@ -103,10 +103,10 @@ func TestLoadBalanceWorksWithSingleEndpoint(t *testing.T) {
}}, }},
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil)
expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil)
expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil)
expectEndpoint(t, loadBalancer, service, "p", "endpoint1:40", nil) expectEndpoint(t, loadBalancer, service, "endpoint1:40", nil)
} }
func stringsInSlice(haystack []string, needles ...string) bool { func stringsInSlice(haystack []string, needles ...string) bool {
@@ -127,8 +127,8 @@ func stringsInSlice(haystack []string, needles ...string) bool {
func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) { func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) {
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"}
endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
@@ -142,26 +142,27 @@ func TestLoadBalanceWorksWithMultipleEndpoints(t *testing.T) {
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, "p"}].endpoints shuffledEndpoints := loadBalancer.services[service].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint:1", "endpoint:2", "endpoint:3") { if !stringsInSlice(shuffledEndpoints, "endpoint:1", "endpoint:2", "endpoint:3") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[2], nil) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], nil)
} }
func TestLoadBalanceWorksWithMultipleEndpointsMultiplePorts(t *testing.T) { func TestLoadBalanceWorksWithMultipleEndpointsMultiplePorts(t *testing.T) {
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") serviceP := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"}
endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "q"}
endpoint, err := loadBalancer.NextEndpoint(serviceP, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace},
Subsets: []api.EndpointSubset{ Subsets: []api.EndpointSubset{
{ {
Addresses: []api.EndpointAddress{{IP: "endpoint1"}, {IP: "endpoint2"}}, Addresses: []api.EndpointAddress{{IP: "endpoint1"}, {IP: "endpoint2"}},
@@ -175,35 +176,36 @@ func TestLoadBalanceWorksWithMultipleEndpointsMultiplePorts(t *testing.T) {
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, "p"}].endpoints shuffledEndpoints := loadBalancer.services[serviceP].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint1:1", "endpoint2:1", "endpoint3:3") { if !stringsInSlice(shuffledEndpoints, "endpoint1:1", "endpoint2:1", "endpoint3:3") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[2], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[2], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil)
shuffledEndpoints = loadBalancer.services[servicePort{service, "q"}].endpoints shuffledEndpoints = loadBalancer.services[serviceQ].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint1:2", "endpoint2:2", "endpoint3:4") { if !stringsInSlice(shuffledEndpoints, "endpoint1:2", "endpoint2:2", "endpoint3:4") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[2], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[2], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil)
} }
func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") serviceP := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"}
endpoint, err := loadBalancer.NextEndpoint(service, "p", nil) serviceQ := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "q"}
endpoint, err := loadBalancer.NextEndpoint(serviceP, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace},
Subsets: []api.EndpointSubset{ Subsets: []api.EndpointSubset{
{ {
Addresses: []api.EndpointAddress{{IP: "endpoint1"}}, Addresses: []api.EndpointAddress{{IP: "endpoint1"}},
@@ -221,28 +223,28 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, "p"}].endpoints shuffledEndpoints := loadBalancer.services[serviceP].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint1:1", "endpoint2:2", "endpoint3:3") { if !stringsInSlice(shuffledEndpoints, "endpoint1:1", "endpoint2:2", "endpoint3:3") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[2], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[2], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil)
shuffledEndpoints = loadBalancer.services[servicePort{service, "q"}].endpoints shuffledEndpoints = loadBalancer.services[serviceQ].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint1:10", "endpoint2:20", "endpoint3:30") { if !stringsInSlice(shuffledEndpoints, "endpoint1:10", "endpoint2:20", "endpoint3:30") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[2], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[2], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil)
// Then update the configuration with one fewer endpoints, make sure // Then update the configuration with one fewer endpoints, make sure
// we start in the beginning again // we start in the beginning again
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace},
Subsets: []api.EndpointSubset{ Subsets: []api.EndpointSubset{
{ {
Addresses: []api.EndpointAddress{{IP: "endpoint4"}}, Addresses: []api.EndpointAddress{{IP: "endpoint4"}},
@@ -256,29 +258,29 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints = loadBalancer.services[servicePort{service, "p"}].endpoints shuffledEndpoints = loadBalancer.services[serviceP].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint4:4", "endpoint5:5") { if !stringsInSlice(shuffledEndpoints, "endpoint4:4", "endpoint5:5") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "p", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceP, shuffledEndpoints[1], nil)
shuffledEndpoints = loadBalancer.services[servicePort{service, "q"}].endpoints shuffledEndpoints = loadBalancer.services[serviceQ].endpoints
if !stringsInSlice(shuffledEndpoints, "endpoint4:40", "endpoint5:50") { if !stringsInSlice(shuffledEndpoints, "endpoint4:40", "endpoint5:50") {
t.Errorf("did not find expected endpoints: %v", shuffledEndpoints) t.Errorf("did not find expected endpoints: %v", shuffledEndpoints)
} }
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[0], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[0], nil)
expectEndpoint(t, loadBalancer, service, "q", shuffledEndpoints[1], nil) expectEndpoint(t, loadBalancer, serviceQ, shuffledEndpoints[1], nil)
// Clear endpoints // Clear endpoints
endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: nil} endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: serviceP.Name, Namespace: serviceP.Namespace}, Subsets: nil}
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
endpoint, err = loadBalancer.NextEndpoint(service, "p", nil) endpoint, err = loadBalancer.NextEndpoint(serviceP, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
@@ -286,15 +288,15 @@ func TestLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) { func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) {
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
fooService := types.NewNamespacedNameOrDie("testnamespace", "foo") fooServiceP := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, "p"}
barService := types.NewNamespacedNameOrDie("testnamespace", "bar") barServiceP := ServicePortName{types.NamespacedName{"testnamespace", "bar"}, "p"}
endpoint, err := loadBalancer.NextEndpoint(fooService, "p", nil) endpoint, err := loadBalancer.NextEndpoint(fooServiceP, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
endpoints := make([]api.Endpoints, 2) endpoints := make([]api.Endpoints, 2)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: fooService.Name, Namespace: fooService.Namespace}, ObjectMeta: api.ObjectMeta{Name: fooServiceP.Name, Namespace: fooServiceP.Namespace},
Subsets: []api.EndpointSubset{ Subsets: []api.EndpointSubset{
{ {
Addresses: []api.EndpointAddress{{IP: "endpoint1"}, {IP: "endpoint2"}, {IP: "endpoint3"}}, Addresses: []api.EndpointAddress{{IP: "endpoint1"}, {IP: "endpoint2"}, {IP: "endpoint3"}},
@@ -303,7 +305,7 @@ func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) {
}, },
} }
endpoints[1] = api.Endpoints{ endpoints[1] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: barService.Name, Namespace: barService.Namespace}, ObjectMeta: api.ObjectMeta{Name: barServiceP.Name, Namespace: barServiceP.Namespace},
Subsets: []api.EndpointSubset{ Subsets: []api.EndpointSubset{
{ {
Addresses: []api.EndpointAddress{{IP: "endpoint4"}, {IP: "endpoint5"}, {IP: "endpoint6"}}, Addresses: []api.EndpointAddress{{IP: "endpoint4"}, {IP: "endpoint5"}, {IP: "endpoint6"}},
@@ -312,54 +314,54 @@ func TestLoadBalanceWorksWithServiceRemoval(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledFooEndpoints := loadBalancer.services[servicePort{fooService, "p"}].endpoints shuffledFooEndpoints := loadBalancer.services[fooServiceP].endpoints
expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[0], nil) expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[0], nil)
expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[1], nil) expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[1], nil)
expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[2], nil) expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[2], nil)
expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[0], nil) expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[0], nil)
expectEndpoint(t, loadBalancer, fooService, "p", shuffledFooEndpoints[1], nil) expectEndpoint(t, loadBalancer, fooServiceP, shuffledFooEndpoints[1], nil)
shuffledBarEndpoints := loadBalancer.services[servicePort{barService, "p"}].endpoints shuffledBarEndpoints := loadBalancer.services[barServiceP].endpoints
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[0], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[0], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[1], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[1], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[2], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[2], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[0], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[0], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[1], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[1], nil)
// Then update the configuration by removing foo // Then update the configuration by removing foo
loadBalancer.OnUpdate(endpoints[1:]) loadBalancer.OnUpdate(endpoints[1:])
endpoint, err = loadBalancer.NextEndpoint(fooService, "p", nil) endpoint, err = loadBalancer.NextEndpoint(fooServiceP, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
// but bar is still there, and we continue RR from where we left off. // but bar is still there, and we continue RR from where we left off.
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[2], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[2], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[0], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[0], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[1], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[1], nil)
expectEndpoint(t, loadBalancer, barService, "p", shuffledBarEndpoints[2], nil) expectEndpoint(t, loadBalancer, barServiceP, shuffledBarEndpoints[2], nil)
} }
func TestStickyLoadBalanceWorksWithSingleEndpoint(t *testing.T) { func TestStickyLoadBalanceWorksWithSingleEndpoint(t *testing.T) {
client1 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} client1 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}
client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0}
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""}
endpoint, err := loadBalancer.NextEndpoint(service, "", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) loadBalancer.NewService(service, api.AffinityTypeClientIP, 0)
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
Subsets: []api.EndpointSubset{{Addresses: []api.EndpointAddress{{IP: "endpoint"}}, Ports: []api.EndpointPort{{Port: 1}}}}, Subsets: []api.EndpointSubset{{Addresses: []api.EndpointAddress{{IP: "endpoint"}}, Ports: []api.EndpointPort{{Port: 1}}}},
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client1) expectEndpoint(t, loadBalancer, service, "endpoint:1", client1)
expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client1) expectEndpoint(t, loadBalancer, service, "endpoint:1", client1)
expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client2) expectEndpoint(t, loadBalancer, service, "endpoint:1", client2)
expectEndpoint(t, loadBalancer, service, "", "endpoint:1", client2) expectEndpoint(t, loadBalancer, service, "endpoint:1", client2)
} }
func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) { func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) {
@@ -367,13 +369,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) {
client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0}
client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0}
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""}
endpoint, err := loadBalancer.NextEndpoint(service, "", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) loadBalancer.NewService(service, api.AffinityTypeClientIP, 0)
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
@@ -385,15 +387,15 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpoints(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints := loadBalancer.services[service].endpoints
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
} }
func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) { func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) {
@@ -401,13 +403,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) {
client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0}
client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0}
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""}
endpoint, err := loadBalancer.NextEndpoint(service, "", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
loadBalancer.NewService(service, "", api.AffinityTypeNone, 0) loadBalancer.NewService(service, api.AffinityTypeNone, 0)
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
@@ -420,15 +422,15 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsStickyNone(t *testing.T) {
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints := loadBalancer.services[service].endpoints
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client3) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client3)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client1)
} }
func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) { func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) {
@@ -439,13 +441,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) {
client5 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 5), Port: 0} client5 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 5), Port: 0}
client6 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 6), Port: 0} client6 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 6), Port: 0}
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""}
endpoint, err := loadBalancer.NextEndpoint(service, "", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) loadBalancer.NewService(service, api.AffinityTypeClientIP, 0)
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
@@ -457,14 +459,14 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints := loadBalancer.services[service].endpoints
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
client1Endpoint := shuffledEndpoints[0] client1Endpoint := shuffledEndpoints[0]
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
client2Endpoint := shuffledEndpoints[1] client2Endpoint := shuffledEndpoints[1]
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3)
client3Endpoint := shuffledEndpoints[2] client3Endpoint := shuffledEndpoints[2]
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
@@ -477,7 +479,7 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints = loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints = loadBalancer.services[service].endpoints
if client1Endpoint == "endpoint:3" { if client1Endpoint == "endpoint:3" {
client1Endpoint = shuffledEndpoints[0] client1Endpoint = shuffledEndpoints[0]
} else if client2Endpoint == "endpoint:3" { } else if client2Endpoint == "endpoint:3" {
@@ -485,9 +487,9 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) {
} else if client3Endpoint == "endpoint:3" { } else if client3Endpoint == "endpoint:3" {
client3Endpoint = shuffledEndpoints[0] client3Endpoint = shuffledEndpoints[0]
} }
expectEndpoint(t, loadBalancer, service, "", client1Endpoint, client1) expectEndpoint(t, loadBalancer, service, client1Endpoint, client1)
expectEndpoint(t, loadBalancer, service, "", client2Endpoint, client2) expectEndpoint(t, loadBalancer, service, client2Endpoint, client2)
expectEndpoint(t, loadBalancer, service, "", client3Endpoint, client3) expectEndpoint(t, loadBalancer, service, client3Endpoint, client3)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
@@ -499,13 +501,13 @@ func TestStickyLoadBalanaceWorksWithMultipleEndpointsRemoveOne(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints = loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints = loadBalancer.services[service].endpoints
expectEndpoint(t, loadBalancer, service, "", client1Endpoint, client1) expectEndpoint(t, loadBalancer, service, client1Endpoint, client1)
expectEndpoint(t, loadBalancer, service, "", client2Endpoint, client2) expectEndpoint(t, loadBalancer, service, client2Endpoint, client2)
expectEndpoint(t, loadBalancer, service, "", client3Endpoint, client3) expectEndpoint(t, loadBalancer, service, client3Endpoint, client3)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client4) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client4)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client5) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client5)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client6) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client6)
} }
func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) { func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
@@ -513,13 +515,13 @@ func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0}
client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0}
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
service := types.NewNamespacedNameOrDie("testnamespace", "foo") service := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""}
endpoint, err := loadBalancer.NextEndpoint(service, "", nil) endpoint, err := loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
loadBalancer.NewService(service, "", api.AffinityTypeClientIP, 0) loadBalancer.NewService(service, api.AffinityTypeClientIP, 0)
endpoints := make([]api.Endpoints, 1) endpoints := make([]api.Endpoints, 1)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace},
@@ -531,14 +533,14 @@ func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints := loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints := loadBalancer.services[service].endpoints
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[2], client3) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[2], client3)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
// Then update the configuration with one fewer endpoints, make sure // Then update the configuration with one fewer endpoints, make sure
// we start in the beginning again // we start in the beginning again
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
@@ -551,19 +553,19 @@ func TestStickyLoadBalanceWorksWithMultipleEndpointsAndUpdates(t *testing.T) {
}, },
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledEndpoints = loadBalancer.services[servicePort{service, ""}].endpoints shuffledEndpoints = loadBalancer.services[service].endpoints
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[0], client1) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[0], client1)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
expectEndpoint(t, loadBalancer, service, "", shuffledEndpoints[1], client2) expectEndpoint(t, loadBalancer, service, shuffledEndpoints[1], client2)
// Clear endpoints // Clear endpoints
endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: nil} endpoints[0] = api.Endpoints{ObjectMeta: api.ObjectMeta{Name: service.Name, Namespace: service.Namespace}, Subsets: nil}
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
endpoint, err = loadBalancer.NextEndpoint(service, "", nil) endpoint, err = loadBalancer.NextEndpoint(service, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
@@ -574,12 +576,12 @@ func TestStickyLoadBalanceWorksWithServiceRemoval(t *testing.T) {
client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0} client2 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 2), Port: 0}
client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0} client3 := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 3), Port: 0}
loadBalancer := NewLoadBalancerRR() loadBalancer := NewLoadBalancerRR()
fooService := types.NewNamespacedNameOrDie("testnamespace", "foo") fooService := ServicePortName{types.NamespacedName{"testnamespace", "foo"}, ""}
endpoint, err := loadBalancer.NextEndpoint(fooService, "", nil) endpoint, err := loadBalancer.NextEndpoint(fooService, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
loadBalancer.NewService(fooService, "", api.AffinityTypeClientIP, 0) loadBalancer.NewService(fooService, api.AffinityTypeClientIP, 0)
endpoints := make([]api.Endpoints, 2) endpoints := make([]api.Endpoints, 2)
endpoints[0] = api.Endpoints{ endpoints[0] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: fooService.Name, Namespace: fooService.Namespace}, ObjectMeta: api.ObjectMeta{Name: fooService.Name, Namespace: fooService.Namespace},
@@ -590,8 +592,8 @@ func TestStickyLoadBalanceWorksWithServiceRemoval(t *testing.T) {
}, },
}, },
} }
barService := types.NewNamespacedNameOrDie("testnamespace", "bar") barService := ServicePortName{types.NamespacedName{"testnamespace", "bar"}, ""}
loadBalancer.NewService(barService, "", api.AffinityTypeClientIP, 0) loadBalancer.NewService(barService, api.AffinityTypeClientIP, 0)
endpoints[1] = api.Endpoints{ endpoints[1] = api.Endpoints{
ObjectMeta: api.ObjectMeta{Name: barService.Name, Namespace: barService.Namespace}, ObjectMeta: api.ObjectMeta{Name: barService.Name, Namespace: barService.Namespace},
Subsets: []api.EndpointSubset{ Subsets: []api.EndpointSubset{
@@ -603,38 +605,38 @@ func TestStickyLoadBalanceWorksWithServiceRemoval(t *testing.T) {
} }
loadBalancer.OnUpdate(endpoints) loadBalancer.OnUpdate(endpoints)
shuffledFooEndpoints := loadBalancer.services[servicePort{fooService, ""}].endpoints shuffledFooEndpoints := loadBalancer.services[fooService].endpoints
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[0], client1) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[0], client1)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[1], client2) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[1], client2)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[2], client3) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[2], client3)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[0], client1) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[0], client1)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[0], client1) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[0], client1)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[1], client2) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[1], client2)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[1], client2) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[1], client2)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[2], client3) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[2], client3)
expectEndpoint(t, loadBalancer, fooService, "", shuffledFooEndpoints[2], client3) expectEndpoint(t, loadBalancer, fooService, shuffledFooEndpoints[2], client3)
shuffledBarEndpoints := loadBalancer.services[servicePort{barService, ""}].endpoints shuffledBarEndpoints := loadBalancer.services[barService].endpoints
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2)
// Then update the configuration by removing foo // Then update the configuration by removing foo
loadBalancer.OnUpdate(endpoints[1:]) loadBalancer.OnUpdate(endpoints[1:])
endpoint, err = loadBalancer.NextEndpoint(fooService, "", nil) endpoint, err = loadBalancer.NextEndpoint(fooService, nil)
if err == nil || len(endpoint) != 0 { if err == nil || len(endpoint) != 0 {
t.Errorf("Didn't fail with non-existent service") t.Errorf("Didn't fail with non-existent service")
} }
// but bar is still there, and we continue RR from where we left off. // but bar is still there, and we continue RR from where we left off.
shuffledBarEndpoints = loadBalancer.services[servicePort{barService, ""}].endpoints shuffledBarEndpoints = loadBalancer.services[barService].endpoints
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[1], client2) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[1], client2)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
expectEndpoint(t, loadBalancer, barService, "", shuffledBarEndpoints[0], client1) expectEndpoint(t, loadBalancer, barService, shuffledBarEndpoints[0], client1)
} }

View File

@@ -576,7 +576,6 @@ func TestEtcdUpdateService(t *testing.T) {
Selector: map[string]string{ Selector: map[string]string{
"baz": "bar", "baz": "bar",
}, },
Protocol: "TCP",
SessionAffinity: "None", SessionAffinity: "None",
}, },
} }

View File

@@ -281,10 +281,12 @@ func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service
if rs.cloud == nil { if rs.cloud == nil {
return fmt.Errorf("requested an external service, but no cloud provider supplied.") return fmt.Errorf("requested an external service, but no cloud provider supplied.")
} }
if service.Spec.Protocol != api.ProtocolTCP {
// TODO: Support UDP here too. ports, err := getTCPPorts(service)
return fmt.Errorf("external load balancers for non TCP services are not currently supported.") if err != nil {
return err
} }
balancer, ok := rs.cloud.TCPLoadBalancer() balancer, ok := rs.cloud.TCPLoadBalancer()
if !ok { if !ok {
return fmt.Errorf("the cloud provider does not support external TCP load balancers.") return fmt.Errorf("the cloud provider does not support external TCP load balancers.")
@@ -302,18 +304,17 @@ func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service
return err return err
} }
name := rs.getLoadbalancerName(ctx, service) name := rs.getLoadbalancerName(ctx, service)
// TODO: We should be able to rely on valid input, and not do defaulting here.
var affinityType api.AffinityType = service.Spec.SessionAffinity var affinityType api.AffinityType = service.Spec.SessionAffinity
if len(service.Spec.PublicIPs) > 0 { if len(service.Spec.PublicIPs) > 0 {
for _, publicIP := range service.Spec.PublicIPs { for _, publicIP := range service.Spec.PublicIPs {
_, err = balancer.CreateTCPLoadBalancer(name, zone.Region, net.ParseIP(publicIP), service.Spec.Port, hostsFromMinionList(hosts), affinityType) _, err = balancer.CreateTCPLoadBalancer(name, zone.Region, net.ParseIP(publicIP), ports, hostsFromMinionList(hosts), affinityType)
if err != nil { if err != nil {
// TODO: have to roll-back any successful calls. // TODO: have to roll-back any successful calls.
return err return err
} }
} }
} else { } else {
endpoint, err := balancer.CreateTCPLoadBalancer(name, zone.Region, nil, service.Spec.Port, hostsFromMinionList(hosts), affinityType) endpoint, err := balancer.CreateTCPLoadBalancer(name, zone.Region, nil, ports, hostsFromMinionList(hosts), affinityType)
if err != nil { if err != nil {
return err return err
} }
@@ -322,6 +323,39 @@ func (rs *REST) createExternalLoadBalancer(ctx api.Context, service *api.Service
return nil return nil
} }
func getTCPPorts(service *api.Service) ([]int, error) {
ports := []int{}
for i := range service.Spec.Ports {
sp := &service.Spec.Ports[i]
if sp.Protocol != api.ProtocolTCP {
// TODO: Support UDP here too.
return nil, fmt.Errorf("external load balancers for non TCP services are not currently supported.")
}
ports = append(ports, sp.Port)
}
return ports, nil
}
func portsEqual(x, y *api.Service) bool {
xPorts, err := getTCPPorts(x)
if err != nil {
return false
}
yPorts, err := getTCPPorts(y)
if err != nil {
return false
}
if len(xPorts) != len(yPorts) {
return false
}
for i := range xPorts {
if xPorts[i] != yPorts[i] {
return false
}
}
return true
}
func (rs *REST) deleteExternalLoadBalancer(ctx api.Context, service *api.Service) error { func (rs *REST) deleteExternalLoadBalancer(ctx api.Context, service *api.Service) error {
if rs.cloud == nil { if rs.cloud == nil {
return fmt.Errorf("requested an external service, but no cloud provider supplied.") return fmt.Errorf("requested an external service, but no cloud provider supplied.")
@@ -348,21 +382,21 @@ func (rs *REST) deleteExternalLoadBalancer(ctx api.Context, service *api.Service
return nil return nil
} }
func externalLoadBalancerNeedsUpdate(old, new *api.Service) bool { func externalLoadBalancerNeedsUpdate(oldService, newService *api.Service) bool {
if !old.Spec.CreateExternalLoadBalancer && !new.Spec.CreateExternalLoadBalancer { if !oldService.Spec.CreateExternalLoadBalancer && !newService.Spec.CreateExternalLoadBalancer {
return false return false
} }
if old.Spec.CreateExternalLoadBalancer != new.Spec.CreateExternalLoadBalancer || if oldService.Spec.CreateExternalLoadBalancer != newService.Spec.CreateExternalLoadBalancer {
old.Spec.Port != new.Spec.Port ||
old.Spec.SessionAffinity != new.Spec.SessionAffinity ||
old.Spec.Protocol != new.Spec.Protocol {
return true return true
} }
if len(old.Spec.PublicIPs) != len(new.Spec.PublicIPs) { if !portsEqual(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity {
return true return true
} }
for i := range old.Spec.PublicIPs { if len(oldService.Spec.PublicIPs) != len(newService.Spec.PublicIPs) {
if old.Spec.PublicIPs[i] != new.Spec.PublicIPs[i] { return true
}
for i := range oldService.Spec.PublicIPs {
if oldService.Spec.PublicIPs[i] != newService.Spec.PublicIPs[i] {
return true return true
} }
} }

View File

@@ -17,6 +17,8 @@ limitations under the License.
package service package service
import ( import (
"bytes"
"encoding/gob"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@@ -52,6 +54,16 @@ func makeIPNet(t *testing.T) *net.IPNet {
return net return net
} }
func deepCloneService(svc *api.Service) *api.Service {
buff := new(bytes.Buffer)
enc := gob.NewEncoder(buff)
dec := gob.NewDecoder(buff)
enc.Encode(svc)
result := new(api.Service)
dec.Decode(result)
return result
}
func TestServiceRegistryCreate(t *testing.T) { func TestServiceRegistryCreate(t *testing.T) {
storage, registry, fakeCloud := NewTestREST(t, nil) storage, registry, fakeCloud := NewTestREST(t, nil)
storage.portalMgr.randomAttempts = 0 storage.portalMgr.randomAttempts = 0
@@ -59,14 +71,19 @@ func TestServiceRegistryCreate(t *testing.T) {
svc := &api.Service{ svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
created_svc, _ := storage.Create(ctx, svc) created_svc, err := storage.Create(ctx, svc)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
created_service := created_svc.(*api.Service) created_service := created_svc.(*api.Service)
if !api.HasObjectMetaSystemFieldValues(&created_service.ObjectMeta) { if !api.HasObjectMetaSystemFieldValues(&created_service.ObjectMeta) {
t.Errorf("storage did not populate object meta field values") t.Errorf("storage did not populate object meta field values")
@@ -98,18 +115,22 @@ func TestServiceStorageValidatesCreate(t *testing.T) {
"empty ID": { "empty ID": {
ObjectMeta: api.ObjectMeta{Name: ""}, ObjectMeta: api.ObjectMeta{Name: ""},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}, },
"empty port": { "empty port": {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Protocol: api.ProtocolTCP,
}},
}, },
}, },
} }
@@ -122,7 +143,6 @@ func TestServiceStorageValidatesCreate(t *testing.T) {
if !errors.IsInvalid(err) { if !errors.IsInvalid(err) {
t.Errorf("Expected to get an invalid resource error, got %v", err) t.Errorf("Expected to get an invalid resource error, got %v", err)
} }
} }
} }
@@ -132,8 +152,11 @@ func TestServiceRegistryUpdate(t *testing.T) {
svc, err := registry.CreateService(ctx, &api.Service{ svc, err := registry.CreateService(ctx, &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz1"}, Selector: map[string]string{"bar": "baz1"},
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}) })
@@ -145,10 +168,12 @@ func TestServiceRegistryUpdate(t *testing.T) {
Name: "foo", Name: "foo",
ResourceVersion: svc.ResourceVersion}, ResourceVersion: svc.ResourceVersion},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz2"}, Selector: map[string]string{"bar": "baz2"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}) })
if err != nil { if err != nil {
@@ -175,27 +200,34 @@ func TestServiceStorageValidatesUpdate(t *testing.T) {
registry.CreateService(ctx, &api.Service{ registry.CreateService(ctx, &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}) })
failureCases := map[string]api.Service{ failureCases := map[string]api.Service{
"empty ID": { "empty ID": {
ObjectMeta: api.ObjectMeta{Name: ""}, ObjectMeta: api.ObjectMeta{Name: ""},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}, },
"invalid selector": { "invalid selector": {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"ThisSelectorFailsValidation": "ok"}, Selector: map[string]string{"ThisSelectorFailsValidation": "ok"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}, },
} }
@@ -216,14 +248,18 @@ func TestServiceRegistryExternalService(t *testing.T) {
svc := &api.Service{ svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
storage.Create(ctx, svc) if _, err := storage.Create(ctx, svc); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" { if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" {
t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls)
} }
@@ -234,7 +270,7 @@ func TestServiceRegistryExternalService(t *testing.T) {
if srv == nil { if srv == nil {
t.Errorf("Failed to find service: %s", svc.Name) t.Errorf("Failed to find service: %s", svc.Name)
} }
if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Port != 6502 { if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Ports[0] != 6502 {
t.Errorf("Unexpected balancer created: %v", fakeCloud.Balancers) t.Errorf("Unexpected balancer created: %v", fakeCloud.Balancers)
} }
} }
@@ -245,15 +281,19 @@ func TestServiceRegistryExternalServiceError(t *testing.T) {
svc := &api.Service{ svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
storage.Create(ctx, svc) if _, err := storage.Create(ctx, svc); err == nil {
t.Fatalf("Unexpected success")
}
if len(fakeCloud.Calls) != 1 || fakeCloud.Calls[0] != "get-zone" { if len(fakeCloud.Calls) != 1 || fakeCloud.Calls[0] != "get-zone" {
t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls)
} }
@@ -269,8 +309,11 @@ func TestServiceRegistryDelete(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
registry.CreateService(ctx, svc) registry.CreateService(ctx, svc)
@@ -291,8 +334,11 @@ func TestServiceRegistryDeleteExternal(t *testing.T) {
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
registry.CreateService(ctx, svc) registry.CreateService(ctx, svc)
@@ -313,32 +359,80 @@ func TestServiceRegistryUpdateExternalService(t *testing.T) {
svc1 := &api.Service{ svc1 := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 6502,
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
CreateExternalLoadBalancer: false, CreateExternalLoadBalancer: false,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
storage.Create(ctx, svc1) if _, err := storage.Create(ctx, svc1); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(fakeCloud.Calls) != 0 { if len(fakeCloud.Calls) != 0 {
t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls)
} }
// Modify load balancer to be external. // Modify load balancer to be external.
svc2 := new(api.Service) svc2 := deepCloneService(svc1)
*svc2 = *svc1
svc2.Spec.CreateExternalLoadBalancer = true svc2.Spec.CreateExternalLoadBalancer = true
storage.Update(ctx, svc2) if _, _, err := storage.Update(ctx, svc2); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" { if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" {
t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls) t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls)
} }
// Change port. // Change port.
svc3 := new(api.Service) svc3 := deepCloneService(svc2)
*svc3 = *svc2 svc3.Spec.Ports[0].Port = 6504
svc3.Spec.Port = 6504 if _, _, err := storage.Update(ctx, svc3); err != nil {
storage.Update(ctx, svc3) t.Fatalf("Unexpected error: %v", err)
}
if len(fakeCloud.Calls) != 6 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" ||
fakeCloud.Calls[2] != "get-zone" || fakeCloud.Calls[3] != "delete" ||
fakeCloud.Calls[4] != "get-zone" || fakeCloud.Calls[5] != "create" {
t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls)
}
}
func TestServiceRegistryUpdateMultiPortExternalService(t *testing.T) {
ctx := api.NewDefaultContext()
storage, _, fakeCloud := NewTestREST(t, nil)
// Create external load balancer.
svc1 := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"},
Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"},
CreateExternalLoadBalancer: true,
SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Name: "p",
Port: 6502,
Protocol: api.ProtocolTCP,
}, {
Name: "q",
Port: 8086,
Protocol: api.ProtocolTCP,
}},
},
}
if _, err := storage.Create(ctx, svc1); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(fakeCloud.Calls) != 2 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" {
t.Errorf("Unexpected call(s): %#v", fakeCloud.Calls)
}
// Modify ports
svc2 := deepCloneService(svc1)
svc2.Spec.Ports[1].Port = 8088
if _, _, err := storage.Update(ctx, svc2); err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(fakeCloud.Calls) != 6 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" || if len(fakeCloud.Calls) != 6 || fakeCloud.Calls[0] != "get-zone" || fakeCloud.Calls[1] != "create" ||
fakeCloud.Calls[2] != "get-zone" || fakeCloud.Calls[3] != "delete" || fakeCloud.Calls[2] != "get-zone" || fakeCloud.Calls[3] != "delete" ||
fakeCloud.Calls[4] != "get-zone" || fakeCloud.Calls[5] != "create" { fakeCloud.Calls[4] != "get-zone" || fakeCloud.Calls[5] != "create" {
@@ -468,9 +562,11 @@ func TestServiceRegistryIPAllocation(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
@@ -487,9 +583,11 @@ func TestServiceRegistryIPAllocation(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "bar"}, ObjectMeta: api.ObjectMeta{Name: "bar"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}} }}
ctx = api.NewDefaultContext() ctx = api.NewDefaultContext()
created_svc2, _ := rest.Create(ctx, svc2) created_svc2, _ := rest.Create(ctx, svc2)
@@ -506,9 +604,11 @@ func TestServiceRegistryIPAllocation(t *testing.T) {
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
PortalIP: "1.2.3.93", PortalIP: "1.2.3.93",
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx = api.NewDefaultContext() ctx = api.NewDefaultContext()
@@ -527,9 +627,11 @@ func TestServiceRegistryIPReallocation(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
@@ -548,9 +650,11 @@ func TestServiceRegistryIPReallocation(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "bar"}, ObjectMeta: api.ObjectMeta{Name: "bar"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx = api.NewDefaultContext() ctx = api.NewDefaultContext()
@@ -572,33 +676,34 @@ func TestServiceRegistryIPUpdate(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
created_svc, _ := rest.Create(ctx, svc) created_svc, _ := rest.Create(ctx, svc)
created_service := created_svc.(*api.Service) created_service := created_svc.(*api.Service)
if created_service.Spec.Port != 6502 { if created_service.Spec.Ports[0].Port != 6502 {
t.Errorf("Expected port 6502, but got %v", created_service.Spec.Port) t.Errorf("Expected port 6502, but got %v", created_service.Spec.Ports[0].Port)
} }
if created_service.Spec.PortalIP != "1.2.3.1" { if created_service.Spec.PortalIP != "1.2.3.1" {
t.Errorf("Unexpected PortalIP: %s", created_service.Spec.PortalIP) t.Errorf("Unexpected PortalIP: %s", created_service.Spec.PortalIP)
} }
update := new(api.Service) update := deepCloneService(created_service)
*update = *created_service update.Spec.Ports[0].Port = 6503
update.Spec.Port = 6503
updated_svc, _, _ := rest.Update(ctx, update) updated_svc, _, _ := rest.Update(ctx, update)
updated_service := updated_svc.(*api.Service) updated_service := updated_svc.(*api.Service)
if updated_service.Spec.Port != 6503 { if updated_service.Spec.Ports[0].Port != 6503 {
t.Errorf("Expected port 6503, but got %v", updated_service.Spec.Port) t.Errorf("Expected port 6503, but got %v", updated_service.Spec.Ports[0].Port)
} }
*update = *created_service update = deepCloneService(created_service)
update.Spec.Port = 6503 update.Spec.Ports[0].Port = 6503
update.Spec.PortalIP = "1.2.3.76" // error update.Spec.PortalIP = "1.2.3.76" // error
_, _, err := rest.Update(ctx, update) _, _, err := rest.Update(ctx, update)
@@ -614,31 +719,32 @@ func TestServiceRegistryIPExternalLoadBalancer(t *testing.T) {
svc := &api.Service{ svc := &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
created_svc, _ := rest.Create(ctx, svc) created_svc, _ := rest.Create(ctx, svc)
created_service := created_svc.(*api.Service) created_service := created_svc.(*api.Service)
if created_service.Spec.Port != 6502 { if created_service.Spec.Ports[0].Port != 6502 {
t.Errorf("Expected port 6502, but got %v", created_service.Spec.Port) t.Errorf("Expected port 6502, but got %v", created_service.Spec.Ports[0].Port)
} }
if created_service.Spec.PortalIP != "1.2.3.1" { if created_service.Spec.PortalIP != "1.2.3.1" {
t.Errorf("Unexpected PortalIP: %s", created_service.Spec.PortalIP) t.Errorf("Unexpected PortalIP: %s", created_service.Spec.PortalIP)
} }
update := new(api.Service) update := deepCloneService(created_service)
*update = *created_service
_, _, err := rest.Update(ctx, update) _, _, err := rest.Update(ctx, update)
if err != nil { if err != nil {
t.Errorf("Unexpected error %v", err) t.Errorf("Unexpected error %v", err)
} }
if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Port != 6502 { if len(fakeCloud.Balancers) != 1 || fakeCloud.Balancers[0].Name != "kubernetes-default-foo" || fakeCloud.Balancers[0].Ports[0] != 6502 {
t.Errorf("Unexpected balancer created: %v", fakeCloud.Balancers) t.Errorf("Unexpected balancer created: %v", fakeCloud.Balancers)
} }
} }
@@ -656,9 +762,11 @@ func TestServiceRegistryIPReloadFromStorage(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
ctx := api.NewDefaultContext() ctx := api.NewDefaultContext()
@@ -667,9 +775,11 @@ func TestServiceRegistryIPReloadFromStorage(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
rest1.Create(ctx, svc) rest1.Create(ctx, svc)
@@ -683,9 +793,11 @@ func TestServiceRegistryIPReloadFromStorage(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: api.ProtocolTCP,
SessionAffinity: api.AffinityTypeNone, SessionAffinity: api.AffinityTypeNone,
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
} }
created_svc, _ := rest2.Create(ctx, svc) created_svc, _ := rest2.Create(ctx, svc)
@@ -743,9 +855,11 @@ func TestCreate(t *testing.T) {
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
PortalIP: "None", PortalIP: "None",
Port: 6502,
Protocol: "TCP",
SessionAffinity: "None", SessionAffinity: "None",
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}, },
// invalid // invalid
@@ -756,10 +870,12 @@ func TestCreate(t *testing.T) {
&api.Service{ &api.Service{
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{"bar": "baz"}, Selector: map[string]string{"bar": "baz"},
Port: 6502,
Protocol: "TCP",
PortalIP: "invalid", PortalIP: "invalid",
SessionAffinity: "None", SessionAffinity: "None",
Ports: []api.ServicePort{{
Port: 6502,
Protocol: api.ProtocolTCP,
}},
}, },
}, },
) )

View File

@@ -73,44 +73,48 @@ func (e *EndpointController) SyncServiceEndpoints() error {
for i := range pods.Items { for i := range pods.Items {
pod := &pods.Items[i] pod := &pods.Items[i]
// TODO: Once v1beta1 and v1beta2 are EOL'ed, this can for i := range service.Spec.Ports {
// assume that service.Spec.TargetPort is populated. servicePort := &service.Spec.Ports[i]
_ = v1beta1.Dependency
_ = v1beta2.Dependency
// TODO: Add multiple-ports to Service and expose them here.
portName := ""
portProto := service.Spec.Protocol
portNum, err := findPort(pod, service)
if err != nil {
glog.Errorf("Failed to find port for service %s/%s: %v", service.Namespace, service.Name, err)
continue
}
if len(pod.Status.PodIP) == 0 {
glog.Errorf("Failed to find an IP for pod %s/%s", pod.Namespace, pod.Name)
continue
}
inService := false // TODO: Once v1beta1 and v1beta2 are EOL'ed, this can
for _, c := range pod.Status.Conditions { // assume that service.Spec.TargetPort is populated.
if c.Type == api.PodReady && c.Status == api.ConditionTrue { _ = v1beta1.Dependency
inService = true _ = v1beta2.Dependency
break
portName := servicePort.Name
portProto := servicePort.Protocol
portNum, err := findPort(pod, servicePort)
if err != nil {
glog.Errorf("Failed to find port for service %s/%s: %v", service.Namespace, service.Name, err)
continue
}
if len(pod.Status.PodIP) == 0 {
glog.Errorf("Failed to find an IP for pod %s/%s", pod.Namespace, pod.Name)
continue
} }
}
if !inService {
glog.V(5).Infof("Pod is out of service: %v/%v", pod.Namespace, pod.Name)
continue
}
epp := api.EndpointPort{Name: portName, Port: portNum, Protocol: portProto} inService := false
epa := api.EndpointAddress{IP: pod.Status.PodIP, TargetRef: &api.ObjectReference{ for _, c := range pod.Status.Conditions {
Kind: "Pod", if c.Type == api.PodReady && c.Status == api.ConditionTrue {
Namespace: pod.ObjectMeta.Namespace, inService = true
Name: pod.ObjectMeta.Name, break
UID: pod.ObjectMeta.UID, }
ResourceVersion: pod.ObjectMeta.ResourceVersion, }
}} if !inService {
subsets = append(subsets, api.EndpointSubset{Addresses: []api.EndpointAddress{epa}, Ports: []api.EndpointPort{epp}}) glog.V(5).Infof("Pod is out of service: %v/%v", pod.Namespace, pod.Name)
continue
}
epp := api.EndpointPort{Name: portName, Port: portNum, Protocol: portProto}
epa := api.EndpointAddress{IP: pod.Status.PodIP, TargetRef: &api.ObjectReference{
Kind: "Pod",
Namespace: pod.ObjectMeta.Namespace,
Name: pod.ObjectMeta.Name,
UID: pod.ObjectMeta.UID,
ResourceVersion: pod.ObjectMeta.ResourceVersion,
}}
subsets = append(subsets, api.EndpointSubset{Addresses: []api.EndpointAddress{epa}, Ports: []api.EndpointPort{epp}})
}
} }
subsets = endpoints.RepackSubsets(subsets) subsets = endpoints.RepackSubsets(subsets)
@@ -169,24 +173,24 @@ func findDefaultPort(pod *api.Pod, servicePort int, proto api.Protocol) int {
// the service's port. If the targetPort is a non-empty string, look that // the service's port. If the targetPort is a non-empty string, look that
// string up in all named ports in all containers in the target pod. If no // string up in all named ports in all containers in the target pod. If no
// match is found, fail. // match is found, fail.
func findPort(pod *api.Pod, service *api.Service) (int, error) { func findPort(pod *api.Pod, svcPort *api.ServicePort) (int, error) {
portName := service.Spec.TargetPort portName := svcPort.TargetPort
switch portName.Kind { switch portName.Kind {
case util.IntstrString: case util.IntstrString:
if len(portName.StrVal) == 0 { if len(portName.StrVal) == 0 {
return findDefaultPort(pod, service.Spec.Port, service.Spec.Protocol), nil return findDefaultPort(pod, svcPort.Port, svcPort.Protocol), nil
} }
name := portName.StrVal name := portName.StrVal
for _, container := range pod.Spec.Containers { for _, container := range pod.Spec.Containers {
for _, port := range container.Ports { for _, port := range container.Ports {
if port.Name == name && port.Protocol == service.Spec.Protocol { if port.Name == name && port.Protocol == svcPort.Protocol {
return port.ContainerPort, nil return port.ContainerPort, nil
} }
} }
} }
case util.IntstrInt: case util.IntstrInt:
if portName.IntVal == 0 { if portName.IntVal == 0 {
return findDefaultPort(pod, service.Spec.Port, service.Spec.Protocol), nil return findDefaultPort(pod, svcPort.Port, svcPort.Protocol), nil
} }
return portName.IntVal, nil return portName.IntVal, nil
} }

View File

@@ -51,7 +51,8 @@ func newPodList(nPods int, nPorts int) *api.PodList {
}, },
} }
for j := 0; j < nPorts; j++ { for j := 0; j < nPorts; j++ {
p.Spec.Containers[0].Ports = append(p.Spec.Containers[0].Ports, api.ContainerPort{ContainerPort: 8080 + j}) p.Spec.Containers[0].Ports = append(p.Spec.Containers[0].Ports,
api.ContainerPort{Name: fmt.Sprintf("port%d", i), ContainerPort: 8080 + j})
} }
pods = append(pods, p) pods = append(pods, p)
} }
@@ -203,7 +204,7 @@ func TestFindPort(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
port, err := findPort(&api.Pod{Spec: api.PodSpec{Containers: tc.containers}}, port, err := findPort(&api.Pod{Spec: api.PodSpec{Containers: tc.containers}},
&api.Service{Spec: api.ServiceSpec{Protocol: "TCP", Port: servicePort, TargetPort: tc.port}}) &api.ServicePort{Protocol: "TCP", Port: servicePort, TargetPort: tc.port})
if err != nil && tc.pass { if err != nil && tc.pass {
t.Errorf("unexpected error for %s: %v", tc.name, err) t.Errorf("unexpected error for %s: %v", tc.name, err)
} }
@@ -277,7 +278,7 @@ func TestSyncEndpointsItemsPreserveNoSelector(t *testing.T) {
Items: []api.Service{ Items: []api.Service{
{ {
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: api.ServiceSpec{}, Spec: api.ServiceSpec{Ports: []api.ServicePort{{Port: 80}}},
}, },
}, },
} }
@@ -310,7 +311,7 @@ func TestSyncEndpointsProtocolTCP(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{}, Selector: map[string]string{},
Protocol: api.ProtocolTCP, Ports: []api.ServicePort{{Port: 80}},
}, },
}, },
}, },
@@ -344,7 +345,7 @@ func TestSyncEndpointsProtocolUDP(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{}, Selector: map[string]string{},
Protocol: api.ProtocolUDP, Ports: []api.ServicePort{{Port: 80}},
}, },
}, },
}, },
@@ -378,6 +379,7 @@ func TestSyncEndpointsItemsEmptySelectorSelectsAll(t *testing.T) {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{}, Selector: map[string]string{},
Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}},
}, },
}, },
}, },
@@ -417,9 +419,8 @@ func TestSyncEndpointsItemsPreexisting(t *testing.T) {
{ {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "bar"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{"foo": "bar"},
"foo": "bar", Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}},
},
}, },
}, },
}, },
@@ -462,9 +463,8 @@ func TestSyncEndpointsItemsPreexistingIdentical(t *testing.T) {
{ {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{"foo": "bar"},
"foo": "bar", Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}},
},
}, },
}, },
}, },
@@ -496,8 +496,10 @@ func TestSyncEndpointsItems(t *testing.T) {
{ {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "other"},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{"foo": "bar"},
"foo": "bar", Ports: []api.ServicePort{
{Name: "port0", Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)},
{Name: "port1", Port: 88, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8088)},
}, },
}, },
}, },
@@ -520,7 +522,8 @@ func TestSyncEndpointsItems(t *testing.T) {
{IP: "1.2.3.6", TargetRef: &api.ObjectReference{Kind: "Pod", Name: "pod2"}}, {IP: "1.2.3.6", TargetRef: &api.ObjectReference{Kind: "Pod", Name: "pod2"}},
}, },
Ports: []api.EndpointPort{ Ports: []api.EndpointPort{
{Port: 8080, Protocol: "TCP"}, {Name: "port0", Port: 8080, Protocol: "TCP"},
{Name: "port1", Port: 8088, Protocol: "TCP"},
}, },
}} }}
data := runtime.EncodeOrDie(testapi.Codec(), &api.Endpoints{ data := runtime.EncodeOrDie(testapi.Codec(), &api.Endpoints{
@@ -540,9 +543,8 @@ func TestSyncEndpointsPodError(t *testing.T) {
{ {
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: api.NamespaceDefault},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Selector: map[string]string{ Selector: map[string]string{"foo": "bar"},
"foo": "bar", Ports: []api.ServicePort{{Port: 80, Protocol: "TCP", TargetPort: util.NewIntOrStringFromInt(8080)}},
},
}, },
}, },
}, },

View File

@@ -1,26 +0,0 @@
/*
Copyright 2015 Google Inc. All rights reserved.
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 types
import "fmt"
func NewNamespacedNameOrDie(namespace, name string) (ret NamespacedName) {
if len(namespace) == 0 || len(name) == 0 {
panic(fmt.Sprintf("invalid call to NewNamespacedNameOrDie(%q, %q)", namespace, name))
}
return NamespacedName{namespace, name}
}

View File

@@ -17,10 +17,106 @@ limitations under the License.
package util package util
import ( import (
"fmt"
"hash/adler32" "hash/adler32"
"testing" "testing"
"github.com/davecgh/go-spew/spew"
) )
type A struct {
x int
y string
}
type B struct {
x []int
y map[string]bool
}
type C struct {
x int
y string
}
func (c C) String() string {
return fmt.Sprintf("%d:%s", c.x, c.y)
}
func TestDeepHashObject(t *testing.T) {
// These types are known to fail object hashing because of how spew implements map keys.
// http://github.com/davecgh/go-spew/issues/30
failureCases := []func() interface{}{
func() interface{} { return map[A]bool{A{8675309, "Jenny"}: true, A{9765683, "!Jenny"}: false} },
func() interface{} { return map[C]bool{C{8675309, "Jenny"}: true, C{9765683, "!Jenny"}: false} },
func() interface{} { return map[*A]bool{&A{8675309, "Jenny"}: true, &A{9765683, "!Jenny"}: false} },
func() interface{} { return map[*C]bool{&C{8675309, "Jenny"}: true, &C{9765683, "!Jenny"}: false} },
}
for _, tc := range failureCases {
hasher := adler32.New()
DeepHashObject(hasher, tc())
first := hasher.Sum32()
alwaysSame := false
for i := 0; i < 100; i++ {
DeepHashObject(hasher, tc())
if hasher.Sum32() != first {
alwaysSame = false
break
}
}
if alwaysSame {
t.Errorf("expected failure for %q", toString(tc()))
}
}
successCases := []func() interface{}{
func() interface{} { return 8675309 },
func() interface{} { return "Jenny, I got your number" },
func() interface{} { return []string{"eight", "six", "seven"} },
func() interface{} { return [...]int{5, 3, 0, 9} },
func() interface{} { return map[int]string{8: "8", 6: "6", 7: "7"} },
func() interface{} { return map[string]int{"5": 5, "3": 3, "0": 0, "9": 9} },
func() interface{} { return A{867, "5309"} },
func() interface{} { return &A{867, "5309"} },
func() interface{} {
return B{[]int{8, 6, 7}, map[string]bool{"5": true, "3": true, "0": true, "9": true}}
},
}
for _, tc := range successCases {
hasher1 := adler32.New()
DeepHashObject(hasher1, tc())
hash1 := hasher1.Sum32()
DeepHashObject(hasher1, tc())
hash2 := hasher1.Sum32()
if hash1 != hash2 {
t.Fatalf("hash of the same object (%q) produced different results: %d vs %d", toString(tc()), hash1, hash2)
}
for i := 0; i < 100; i++ {
hasher2 := adler32.New()
DeepHashObject(hasher1, tc())
hash1a := hasher1.Sum32()
DeepHashObject(hasher2, tc())
hash2a := hasher2.Sum32()
if hash1a != hash1 {
t.Errorf("repeated hash of the same object (%q) produced different results: %d vs %d", toString(tc()), hash1, hash1a)
}
if hash2a != hash2 {
t.Errorf("repeated hash of the same object (%q) produced different results: %d vs %d", toString(tc()), hash2, hash2a)
}
if hash1a != hash2a {
t.Errorf("hash of the same object produced (%q) different results: %d vs %d", toString(tc()), hash1a, hash2a)
}
}
}
}
func toString(obj interface{}) string {
return spew.Sprintf("%#v", obj)
}
type wheel struct { type wheel struct {
radius uint32 radius uint32
} }

View File

@@ -77,8 +77,11 @@ var _ = Describe("Networking", func() {
}, },
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8080, Ports: []api.ServicePort{{
TargetPort: util.NewIntOrStringFromInt(8080), Protocol: "TCP",
Port: 8080,
TargetPort: util.NewIntOrStringFromInt(8080),
}},
Selector: map[string]string{ Selector: map[string]string{
"name": name, "name": name,
}, },

View File

@@ -307,8 +307,10 @@ var _ = Describe("Pods", func() {
}, },
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 8765, Ports: []api.ServicePort{{
TargetPort: util.NewIntOrStringFromInt(8080), Port: 8765,
TargetPort: util.NewIntOrStringFromInt(8080),
}},
Selector: map[string]string{ Selector: map[string]string{
"name": serverName, "name": serverName,
}, },

View File

@@ -204,9 +204,11 @@ var _ = Describe("Services", func() {
Name: serviceName, Name: serviceName,
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 80, Selector: labels,
Selector: labels, Ports: []api.ServicePort{{
TargetPort: util.NewIntOrStringFromInt(80), Port: 80,
TargetPort: util.NewIntOrStringFromInt(80),
}},
}, },
} }
_, err := c.Services(ns).Create(service) _, err := c.Services(ns).Create(service)
@@ -264,9 +266,11 @@ var _ = Describe("Services", func() {
Name: serviceName, Name: serviceName,
}, },
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 80, Selector: labels,
Selector: labels, Ports: []api.ServicePort{{
TargetPort: util.NewIntOrStringFromInt(80), Port: 80,
TargetPort: util.NewIntOrStringFromInt(80),
}},
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
}, },
} }
@@ -287,7 +291,7 @@ var _ = Describe("Services", func() {
Failf("got unexpected number (%d) of public IPs for externally load balanced service: %v", result.Spec.PublicIPs, result) Failf("got unexpected number (%d) of public IPs for externally load balanced service: %v", result.Spec.PublicIPs, result)
} }
ip := result.Spec.PublicIPs[0] ip := result.Spec.PublicIPs[0]
port := result.Spec.Port port := result.Spec.Ports[0].Port
pod := &api.Pod{ pod := &api.Pod{
TypeMeta: api.TypeMeta{ TypeMeta: api.TypeMeta{
@@ -351,9 +355,11 @@ var _ = Describe("Services", func() {
service := &api.Service{ service := &api.Service{
ObjectMeta: api.ObjectMeta{}, ObjectMeta: api.ObjectMeta{},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
Port: 80, Selector: labels,
Selector: labels, Ports: []api.ServicePort{{
TargetPort: util.NewIntOrStringFromInt(80), Port: 80,
TargetPort: util.NewIntOrStringFromInt(80),
}},
CreateExternalLoadBalancer: true, CreateExternalLoadBalancer: true,
}, },
} }