Merge pull request #8707 from justinsb/nodeport
WIP: ServiceType & NodePort work
This commit is contained in:
@@ -86,6 +86,7 @@ type APIServer struct {
|
|||||||
CorsAllowedOriginList util.StringList
|
CorsAllowedOriginList util.StringList
|
||||||
AllowPrivileged bool
|
AllowPrivileged bool
|
||||||
PortalNet util.IPNet // TODO: make this a list
|
PortalNet util.IPNet // TODO: make this a list
|
||||||
|
ServiceNodePorts util.PortRange
|
||||||
EnableLogsSupport bool
|
EnableLogsSupport bool
|
||||||
MasterServiceNamespace string
|
MasterServiceNamespace string
|
||||||
RuntimeConfig util.ConfigurationMap
|
RuntimeConfig util.ConfigurationMap
|
||||||
@@ -183,6 +184,7 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
|
|||||||
fs.Var(&s.CorsAllowedOriginList, "cors-allowed-origins", "List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled.")
|
fs.Var(&s.CorsAllowedOriginList, "cors-allowed-origins", "List of allowed origins for CORS, comma separated. An allowed origin can be a regular expression to support subdomain matching. If this list is empty CORS will not be enabled.")
|
||||||
fs.BoolVar(&s.AllowPrivileged, "allow-privileged", s.AllowPrivileged, "If true, allow privileged containers.")
|
fs.BoolVar(&s.AllowPrivileged, "allow-privileged", s.AllowPrivileged, "If true, allow privileged containers.")
|
||||||
fs.Var(&s.PortalNet, "portal-net", "A CIDR notation IP range from which to assign portal IPs. This must not overlap with any IP ranges assigned to nodes for pods.")
|
fs.Var(&s.PortalNet, "portal-net", "A CIDR notation IP range from which to assign portal IPs. This must not overlap with any IP ranges assigned to nodes for pods.")
|
||||||
|
fs.Var(&s.ServiceNodePorts, "service-node-ports", "A port range to reserve for services with NodePort visibility. Example: '30000-32767'. Inclusive at both ends of the range.")
|
||||||
fs.StringVar(&s.MasterServiceNamespace, "master-service-namespace", s.MasterServiceNamespace, "The namespace from which the kubernetes master services should be injected into pods")
|
fs.StringVar(&s.MasterServiceNamespace, "master-service-namespace", s.MasterServiceNamespace, "The namespace from which the kubernetes master services should be injected into pods")
|
||||||
fs.Var(&s.RuntimeConfig, "runtime-config", "A set of key=value pairs that describe runtime configuration that may be passed to the apiserver. api/<version> key can be used to turn on/off specific api versions. api/all and api/legacy are special keys to control all and legacy api versions respectively.")
|
fs.Var(&s.RuntimeConfig, "runtime-config", "A set of key=value pairs that describe runtime configuration that may be passed to the apiserver. api/<version> key can be used to turn on/off specific api versions. api/all and api/legacy are special keys to control all and legacy api versions respectively.")
|
||||||
client.BindKubeletClientConfigFlags(fs, &s.KubeletConfig)
|
client.BindKubeletClientConfigFlags(fs, &s.KubeletConfig)
|
||||||
|
|||||||
@@ -629,6 +629,7 @@ _kubectl_expose()
|
|||||||
flags+=("--target-port=")
|
flags+=("--target-port=")
|
||||||
flags+=("--template=")
|
flags+=("--template=")
|
||||||
two_word_flags+=("-t")
|
two_word_flags+=("-t")
|
||||||
|
flags+=("--type=")
|
||||||
|
|
||||||
must_have_one_flag=()
|
must_have_one_flag=()
|
||||||
must_have_one_flag+=("--port=")
|
must_have_one_flag+=("--port=")
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ selector for a new Service on the specified port. If no labels are specified, th
|
|||||||
re-use the labels from the resource it exposes.
|
re-use the labels from the resource it exposes.
|
||||||
|
|
||||||
```
|
```
|
||||||
kubectl expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--create-external-load-balancer=bool]
|
kubectl expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
@@ -32,7 +32,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream
|
|||||||
|
|
||||||
```
|
```
|
||||||
--container-port="": Synonym for --target-port
|
--container-port="": Synonym for --target-port
|
||||||
--create-external-load-balancer=false: If true, create an external load balancer for this service. Implementation is cloud provider dependent. Default is 'false'.
|
--create-external-load-balancer=false: If true, create an external load balancer for this service (trumped by --type). Implementation is cloud provider dependent. Default is 'false'.
|
||||||
--dry-run=false: If true, only print the object that would be sent, without creating it.
|
--dry-run=false: If true, only print the object that would be sent, without creating it.
|
||||||
--generator="service/v1": The name of the API generator to use. Default is 'service/v1'.
|
--generator="service/v1": The name of the API generator to use. Default is 'service/v1'.
|
||||||
-h, --help=false: help for expose
|
-h, --help=false: help for expose
|
||||||
@@ -48,6 +48,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream
|
|||||||
--selector="": A label selector to use for this service. If empty (the default) infer the selector from the replication controller.
|
--selector="": A label selector to use for this service. If empty (the default) infer the selector from the replication controller.
|
||||||
--target-port="": Name or number for the port on the container that the service should direct traffic to. Optional.
|
--target-port="": Name or number for the port on the container that the service should direct traffic to. Optional.
|
||||||
-t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]
|
-t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]
|
||||||
|
--type="": Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP' unless --create-external-load-balancer is specified.
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ re\-use the labels from the resource it exposes.
|
|||||||
|
|
||||||
.PP
|
.PP
|
||||||
\fB\-\-create\-external\-load\-balancer\fP=false
|
\fB\-\-create\-external\-load\-balancer\fP=false
|
||||||
If true, create an external load balancer for this service. Implementation is cloud provider dependent. Default is 'false'.
|
If true, create an external load balancer for this service (trumped by \-\-type). Implementation is cloud provider dependent. Default is 'false'.
|
||||||
|
|
||||||
.PP
|
.PP
|
||||||
\fB\-\-dry\-run\fP=false
|
\fB\-\-dry\-run\fP=false
|
||||||
@@ -91,6 +91,10 @@ re\-use the labels from the resource it exposes.
|
|||||||
Template string or path to template file to use when \-o=template or \-o=templatefile. The template format is golang templates [
|
Template string or path to template file to use when \-o=template or \-o=templatefile. The template format is golang templates [
|
||||||
\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]]
|
\[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]]
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-type\fP=""
|
||||||
|
Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP' unless \-\-create\-external\-load\-balancer is specified.
|
||||||
|
|
||||||
|
|
||||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||||
.PP
|
.PP
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ On some platforms (for example Google Compute Engine) the kubectl command can in
|
|||||||
to do this run:
|
to do this run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
kubectl expose rc my-nginx --port=80 --create-external-load-balancer
|
kubectl expose rc my-nginx --port=80 --type=LoadBalancer
|
||||||
```
|
```
|
||||||
|
|
||||||
This should print the service that has been created, and map an external IP address to the service.
|
This should print the service that has been created, and map an external IP address to the service.
|
||||||
|
|||||||
@@ -141,3 +141,40 @@ func HashObject(obj runtime.Object, codec runtime.Codec) (string, error) {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("%x", md5.Sum(data)), nil
|
return fmt.Sprintf("%x", md5.Sum(data)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: make method on LoadBalancerStatus?
|
||||||
|
func LoadBalancerStatusEqual(l, r *LoadBalancerStatus) bool {
|
||||||
|
return ingressSliceEqual(l.Ingress, r.Ingress)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingressSliceEqual(lhs, rhs []LoadBalancerIngress) bool {
|
||||||
|
if len(lhs) != len(rhs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range lhs {
|
||||||
|
if !ingressEqual(&lhs[i], &rhs[i]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ingressEqual(lhs, rhs *LoadBalancerIngress) bool {
|
||||||
|
if lhs.IP != rhs.IP {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.Hostname != rhs.Hostname {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make method on LoadBalancerStatus?
|
||||||
|
func LoadBalancerStatusDeepCopy(lb *LoadBalancerStatus) *LoadBalancerStatus {
|
||||||
|
c := &LoadBalancerStatus{}
|
||||||
|
c.Ingress = make([]LoadBalancerIngress, len(lb.Ingress))
|
||||||
|
for i := range lb.Ingress {
|
||||||
|
c.Ingress[i] = lb.Ingress[i]
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func makeValidService() api.Service {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"key": "val"},
|
Selector: map[string]string{"key": "val"},
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}},
|
Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,10 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer {
|
|||||||
types := []api.ServiceAffinity{api.ServiceAffinityClientIP, api.ServiceAffinityNone}
|
types := []api.ServiceAffinity{api.ServiceAffinityClientIP, api.ServiceAffinityNone}
|
||||||
*p = types[c.Rand.Intn(len(types))]
|
*p = types[c.Rand.Intn(len(types))]
|
||||||
},
|
},
|
||||||
|
func(p *api.ServiceType, c fuzz.Continue) {
|
||||||
|
types := []api.ServiceType{api.ServiceTypeClusterIP, api.ServiceTypeNodePort, api.ServiceTypeLoadBalancer}
|
||||||
|
*p = types[c.Rand.Intn(len(types))]
|
||||||
|
},
|
||||||
func(ct *api.Container, c fuzz.Continue) {
|
func(ct *api.Container, c fuzz.Continue) {
|
||||||
c.FuzzNoCustom(ct) // fuzz self without calling this function again
|
c.FuzzNoCustom(ct) // fuzz self without calling this function again
|
||||||
ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty
|
ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty
|
||||||
|
|||||||
@@ -1011,8 +1011,49 @@ const (
|
|||||||
ServiceAffinityNone ServiceAffinity = "None"
|
ServiceAffinityNone ServiceAffinity = "None"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service Type string describes ingress methods for a service
|
||||||
|
type ServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ServiceTypeClusterIP means a service will only be accessible inside the
|
||||||
|
// cluster, via the portal IP.
|
||||||
|
ServiceTypeClusterIP ServiceType = "ClusterIP"
|
||||||
|
|
||||||
|
// ServiceTypeNodePort means a service will be exposed on one port of
|
||||||
|
// every node, in addition to 'ClusterIP' type.
|
||||||
|
ServiceTypeNodePort ServiceType = "NodePort"
|
||||||
|
|
||||||
|
// ServiceTypeLoadBalancer means a service will be exposed via an
|
||||||
|
// external load balancer (if the cloud provider supports it), in addition
|
||||||
|
// to 'NodePort' type.
|
||||||
|
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"
|
||||||
|
)
|
||||||
|
|
||||||
// ServiceStatus represents the current status of a service
|
// ServiceStatus represents the current status of a service
|
||||||
type ServiceStatus struct{}
|
type ServiceStatus struct {
|
||||||
|
// LoadBalancer contains the current status of the load-balancer,
|
||||||
|
// if one is present.
|
||||||
|
LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerStatus represents the status of a load-balancer
|
||||||
|
type LoadBalancerStatus struct {
|
||||||
|
// Ingress is a list containing ingress points for the load-balancer;
|
||||||
|
// traffic intended for the service should be sent to these ingress points.
|
||||||
|
Ingress []LoadBalancerIngress `json:"ingress,omitempty" description:"load-balancer ingress points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerIngress represents the status of a load-balancer ingress point:
|
||||||
|
// traffic intended for the service should be sent to an ingress point.
|
||||||
|
type LoadBalancerIngress struct {
|
||||||
|
// IP is set for load-balancer ingress points that are IP based
|
||||||
|
// (typically GCE or OpenStack load-balancers)
|
||||||
|
IP string `json:"ip,omitempty" description:"IP address of ingress point"`
|
||||||
|
|
||||||
|
// Hostname is set for load-balancer ingress points that are DNS based
|
||||||
|
// (typically AWS load-balancers)
|
||||||
|
Hostname string `json:"hostname,omitempty" description:"hostname of ingress point"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -1031,14 +1072,13 @@ type ServiceSpec struct {
|
|||||||
// None can be specified for headless services when proxying is not required
|
// None can be specified for headless services when proxying is not required
|
||||||
PortalIP string `json:"portalIP,omitempty"`
|
PortalIP string `json:"portalIP,omitempty"`
|
||||||
|
|
||||||
// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.
|
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
|
||||||
CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty"`
|
Type ServiceType `json:"type,omitempty"`
|
||||||
// PublicIPs are used by external load balancers, or can be set by
|
|
||||||
|
// DeprecatedPublicIPs are deprecated and silently ignored.
|
||||||
|
// Old behaviour: PublicIPs are used by external load balancers, or can be set by
|
||||||
// users to handle external traffic that arrives at a node.
|
// users to handle external traffic that arrives at a node.
|
||||||
// For load balancers, the publicIP will usually be the IP address of the load balancer,
|
DeprecatedPublicIPs []string `json:"deprecatedPublicIPs,omitempty"`
|
||||||
// but some load balancers (notably AWS ELB) use a hostname instead of an IP address.
|
|
||||||
// For hostnames, the user will use a CNAME record (instead of using an A record with the IP)
|
|
||||||
PublicIPs []string `json:"publicIPs,omitempty"`
|
|
||||||
|
|
||||||
// Required: Supports "ClientIP" and "None". Used to maintain session affinity.
|
// Required: Supports "ClientIP" and "None". Used to maintain session affinity.
|
||||||
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty"`
|
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty"`
|
||||||
@@ -1064,6 +1104,10 @@ type ServicePort struct {
|
|||||||
// of v1beta3 the default value is the sames as the Port field (an
|
// of v1beta3 the default value is the sames as the Port field (an
|
||||||
// identity map).
|
// identity map).
|
||||||
TargetPort util.IntOrString `json:"targetPort"`
|
TargetPort util.IntOrString `json:"targetPort"`
|
||||||
|
|
||||||
|
// The port on each node on which this service is exposed.
|
||||||
|
// Default is to auto-allocate a port if the ServiceType of this Service requires one.
|
||||||
|
NodePort int `json:"nodePort" description:"the port on each node on which this service is exposed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -812,6 +812,32 @@ func convert_api_ListOptions_To_v1_ListOptions(in *api.ListOptions, out *ListOpt
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_api_LoadBalancerIngress_To_v1_LoadBalancerIngress(in *api.LoadBalancerIngress, out *LoadBalancerIngress, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*api.LoadBalancerIngress))(in)
|
||||||
|
}
|
||||||
|
out.IP = in.IP
|
||||||
|
out.Hostname = in.Hostname
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_api_LoadBalancerStatus_To_v1_LoadBalancerStatus(in *api.LoadBalancerStatus, out *LoadBalancerStatus, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*api.LoadBalancerStatus))(in)
|
||||||
|
}
|
||||||
|
if in.Ingress != nil {
|
||||||
|
out.Ingress = make([]LoadBalancerIngress, len(in.Ingress))
|
||||||
|
for i := range in.Ingress {
|
||||||
|
if err := convert_api_LoadBalancerIngress_To_v1_LoadBalancerIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Ingress = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func convert_api_LocalObjectReference_To_v1_LocalObjectReference(in *api.LocalObjectReference, out *LocalObjectReference, s conversion.Scope) error {
|
func convert_api_LocalObjectReference_To_v1_LocalObjectReference(in *api.LocalObjectReference, out *LocalObjectReference, s conversion.Scope) error {
|
||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*api.LocalObjectReference))(in)
|
defaulting.(func(*api.LocalObjectReference))(in)
|
||||||
@@ -2045,6 +2071,7 @@ func convert_api_ServicePort_To_v1_ServicePort(in *api.ServicePort, out *Service
|
|||||||
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
out.NodePort = in.NodePort
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2071,14 +2098,14 @@ func convert_api_ServiceSpec_To_v1_ServiceSpec(in *api.ServiceSpec, out *Service
|
|||||||
out.Selector = nil
|
out.Selector = nil
|
||||||
}
|
}
|
||||||
out.PortalIP = in.PortalIP
|
out.PortalIP = in.PortalIP
|
||||||
out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer
|
out.Type = ServiceType(in.Type)
|
||||||
if in.PublicIPs != nil {
|
if in.DeprecatedPublicIPs != nil {
|
||||||
out.PublicIPs = make([]string, len(in.PublicIPs))
|
out.DeprecatedPublicIPs = make([]string, len(in.DeprecatedPublicIPs))
|
||||||
for i := range in.PublicIPs {
|
for i := range in.DeprecatedPublicIPs {
|
||||||
out.PublicIPs[i] = in.PublicIPs[i]
|
out.DeprecatedPublicIPs[i] = in.DeprecatedPublicIPs[i]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
out.PublicIPs = nil
|
out.DeprecatedPublicIPs = nil
|
||||||
}
|
}
|
||||||
out.SessionAffinity = ServiceAffinity(in.SessionAffinity)
|
out.SessionAffinity = ServiceAffinity(in.SessionAffinity)
|
||||||
return nil
|
return nil
|
||||||
@@ -2088,6 +2115,9 @@ func convert_api_ServiceStatus_To_v1_ServiceStatus(in *api.ServiceStatus, out *S
|
|||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*api.ServiceStatus))(in)
|
defaulting.(func(*api.ServiceStatus))(in)
|
||||||
}
|
}
|
||||||
|
if err := convert_api_LoadBalancerStatus_To_v1_LoadBalancerStatus(&in.LoadBalancer, &out.LoadBalancer, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3038,6 +3068,32 @@ func convert_v1_ListOptions_To_api_ListOptions(in *ListOptions, out *api.ListOpt
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_v1_LoadBalancerIngress_To_api_LoadBalancerIngress(in *LoadBalancerIngress, out *api.LoadBalancerIngress, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*LoadBalancerIngress))(in)
|
||||||
|
}
|
||||||
|
out.IP = in.IP
|
||||||
|
out.Hostname = in.Hostname
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_v1_LoadBalancerStatus_To_api_LoadBalancerStatus(in *LoadBalancerStatus, out *api.LoadBalancerStatus, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*LoadBalancerStatus))(in)
|
||||||
|
}
|
||||||
|
if in.Ingress != nil {
|
||||||
|
out.Ingress = make([]api.LoadBalancerIngress, len(in.Ingress))
|
||||||
|
for i := range in.Ingress {
|
||||||
|
if err := convert_v1_LoadBalancerIngress_To_api_LoadBalancerIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Ingress = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func convert_v1_LocalObjectReference_To_api_LocalObjectReference(in *LocalObjectReference, out *api.LocalObjectReference, s conversion.Scope) error {
|
func convert_v1_LocalObjectReference_To_api_LocalObjectReference(in *LocalObjectReference, out *api.LocalObjectReference, s conversion.Scope) error {
|
||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*LocalObjectReference))(in)
|
defaulting.(func(*LocalObjectReference))(in)
|
||||||
@@ -4271,6 +4327,7 @@ func convert_v1_ServicePort_To_api_ServicePort(in *ServicePort, out *api.Service
|
|||||||
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
out.NodePort = in.NodePort
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4297,14 +4354,14 @@ func convert_v1_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.Service
|
|||||||
out.Selector = nil
|
out.Selector = nil
|
||||||
}
|
}
|
||||||
out.PortalIP = in.PortalIP
|
out.PortalIP = in.PortalIP
|
||||||
out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer
|
out.Type = api.ServiceType(in.Type)
|
||||||
if in.PublicIPs != nil {
|
if in.DeprecatedPublicIPs != nil {
|
||||||
out.PublicIPs = make([]string, len(in.PublicIPs))
|
out.DeprecatedPublicIPs = make([]string, len(in.DeprecatedPublicIPs))
|
||||||
for i := range in.PublicIPs {
|
for i := range in.DeprecatedPublicIPs {
|
||||||
out.PublicIPs[i] = in.PublicIPs[i]
|
out.DeprecatedPublicIPs[i] = in.DeprecatedPublicIPs[i]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
out.PublicIPs = nil
|
out.DeprecatedPublicIPs = nil
|
||||||
}
|
}
|
||||||
out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity)
|
out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity)
|
||||||
return nil
|
return nil
|
||||||
@@ -4314,6 +4371,9 @@ func convert_v1_ServiceStatus_To_api_ServiceStatus(in *ServiceStatus, out *api.S
|
|||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*ServiceStatus))(in)
|
defaulting.(func(*ServiceStatus))(in)
|
||||||
}
|
}
|
||||||
|
if err := convert_v1_LoadBalancerStatus_To_api_LoadBalancerStatus(&in.LoadBalancer, &out.LoadBalancer, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4520,6 +4580,8 @@ func init() {
|
|||||||
convert_api_ListMeta_To_v1_ListMeta,
|
convert_api_ListMeta_To_v1_ListMeta,
|
||||||
convert_api_ListOptions_To_v1_ListOptions,
|
convert_api_ListOptions_To_v1_ListOptions,
|
||||||
convert_api_List_To_v1_List,
|
convert_api_List_To_v1_List,
|
||||||
|
convert_api_LoadBalancerIngress_To_v1_LoadBalancerIngress,
|
||||||
|
convert_api_LoadBalancerStatus_To_v1_LoadBalancerStatus,
|
||||||
convert_api_LocalObjectReference_To_v1_LocalObjectReference,
|
convert_api_LocalObjectReference_To_v1_LocalObjectReference,
|
||||||
convert_api_NFSVolumeSource_To_v1_NFSVolumeSource,
|
convert_api_NFSVolumeSource_To_v1_NFSVolumeSource,
|
||||||
convert_api_NamespaceList_To_v1_NamespaceList,
|
convert_api_NamespaceList_To_v1_NamespaceList,
|
||||||
@@ -4629,6 +4691,8 @@ func init() {
|
|||||||
convert_v1_ListMeta_To_api_ListMeta,
|
convert_v1_ListMeta_To_api_ListMeta,
|
||||||
convert_v1_ListOptions_To_api_ListOptions,
|
convert_v1_ListOptions_To_api_ListOptions,
|
||||||
convert_v1_List_To_api_List,
|
convert_v1_List_To_api_List,
|
||||||
|
convert_v1_LoadBalancerIngress_To_api_LoadBalancerIngress,
|
||||||
|
convert_v1_LoadBalancerStatus_To_api_LoadBalancerStatus,
|
||||||
convert_v1_LocalObjectReference_To_api_LocalObjectReference,
|
convert_v1_LocalObjectReference_To_api_LocalObjectReference,
|
||||||
convert_v1_NFSVolumeSource_To_api_NFSVolumeSource,
|
convert_v1_NFSVolumeSource_To_api_NFSVolumeSource,
|
||||||
convert_v1_NamespaceList_To_api_NamespaceList,
|
convert_v1_NamespaceList_To_api_NamespaceList,
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ func addDefaultingFuncs() {
|
|||||||
if obj.SessionAffinity == "" {
|
if obj.SessionAffinity == "" {
|
||||||
obj.SessionAffinity = ServiceAffinityNone
|
obj.SessionAffinity = ServiceAffinityNone
|
||||||
}
|
}
|
||||||
|
if obj.Type == "" {
|
||||||
|
obj.Type = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
for i := range obj.Ports {
|
for i := range obj.Ports {
|
||||||
sp := &obj.Ports[i]
|
sp := &obj.Ports[i]
|
||||||
if sp.Protocol == "" {
|
if sp.Protocol == "" {
|
||||||
|
|||||||
@@ -233,7 +233,10 @@ func TestSetDefaultService(t *testing.T) {
|
|||||||
obj2 := roundTrip(t, runtime.Object(svc))
|
obj2 := roundTrip(t, runtime.Object(svc))
|
||||||
svc2 := obj2.(*versioned.Service)
|
svc2 := obj2.(*versioned.Service)
|
||||||
if svc2.Spec.SessionAffinity != versioned.ServiceAffinityNone {
|
if svc2.Spec.SessionAffinity != versioned.ServiceAffinityNone {
|
||||||
t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity)
|
t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity)
|
||||||
|
}
|
||||||
|
if svc2.Spec.Type != versioned.ServiceTypeClusterIP {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Spec.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -993,8 +993,49 @@ const (
|
|||||||
ServiceAffinityNone ServiceAffinity = "None"
|
ServiceAffinityNone ServiceAffinity = "None"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service Type string describes ingress methods for a service
|
||||||
|
type ServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ServiceTypeClusterIP means a service will only be accessible inside the
|
||||||
|
// cluster, via the portal IP.
|
||||||
|
ServiceTypeClusterIP ServiceType = "ClusterIP"
|
||||||
|
|
||||||
|
// ServiceTypeNodePort means a service will be exposed on one port of
|
||||||
|
// every node, in addition to 'ClusterIP' type.
|
||||||
|
ServiceTypeNodePort ServiceType = "NodePort"
|
||||||
|
|
||||||
|
// ServiceTypeLoadBalancer means a service will be exposed via an
|
||||||
|
// external load balancer (if the cloud provider supports it), in addition
|
||||||
|
// to 'NodePort' type.
|
||||||
|
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"
|
||||||
|
)
|
||||||
|
|
||||||
// ServiceStatus represents the current status of a service
|
// ServiceStatus represents the current status of a service
|
||||||
type ServiceStatus struct{}
|
type ServiceStatus struct {
|
||||||
|
// LoadBalancer contains the current status of the load-balancer,
|
||||||
|
// if one is present.
|
||||||
|
LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty" description:"status of load-balancer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerStatus represents the status of a load-balancer
|
||||||
|
type LoadBalancerStatus struct {
|
||||||
|
// Ingress is a list containing ingress points for the load-balancer;
|
||||||
|
// traffic intended for the service should be sent to these ingress points.
|
||||||
|
Ingress []LoadBalancerIngress `json:"ingress,omitempty" description:"load-balancer ingress points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerIngress represents the status of a load-balancer ingress point:
|
||||||
|
// traffic intended for the service should be sent to an ingress point.
|
||||||
|
type LoadBalancerIngress struct {
|
||||||
|
// IP is set for load-balancer ingress points that are IP based
|
||||||
|
// (typically GCE or OpenStack load-balancers)
|
||||||
|
IP string `json:"ip,omitempty" description:"IP address of ingress point"`
|
||||||
|
|
||||||
|
// Hostname is set for load-balancer ingress points that are DNS based
|
||||||
|
// (typically AWS load-balancers)
|
||||||
|
Hostname string `json:"hostname,omitempty" description:"hostname of ingress point"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -1011,12 +1052,12 @@ type ServiceSpec struct {
|
|||||||
// None can be specified for headless services when proxying is not required
|
// None can be specified for headless services when proxying is not required
|
||||||
PortalIP string `json:"portalIP,omitempty description: IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required"`
|
PortalIP string `json:"portalIP,omitempty description: IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required"`
|
||||||
|
|
||||||
// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.
|
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
|
||||||
CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"`
|
Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"`
|
||||||
|
|
||||||
// PublicIPs are used by external load balancers, or can be set by
|
// Deprecated. PublicIPs are used by external load balancers, or can be set by
|
||||||
// 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"`
|
DeprecatedPublicIPs []string `json:"deprecatedPublicIPs,omitempty" description:"deprecated. externally visible IPs (e.g. load balancers) that should be proxied to this service"`
|
||||||
|
|
||||||
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
|
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
|
||||||
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
|
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
|
||||||
@@ -1041,6 +1082,10 @@ type ServicePort struct {
|
|||||||
// target Pod's container ports. If this is not specified, the value
|
// target Pod's container ports. If this is not specified, the value
|
||||||
// of Port is used (an identity map).
|
// of Port is used (an identity map).
|
||||||
TargetPort util.IntOrString `json:"targetPort,omitempty" description:"the port to access on the pods targeted by the service; defaults to the service port"`
|
TargetPort util.IntOrString `json:"targetPort,omitempty" description:"the port to access on the pods targeted by the service; defaults to the service port"`
|
||||||
|
|
||||||
|
// The port on each node on which this service is exposed.
|
||||||
|
// Default is to auto-allocate a port if the ServiceType of this Service requires one.
|
||||||
|
NodePort int `json:"nodePort" description:"the port on each node on which this service is exposed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -774,19 +774,28 @@ func addConversionFuncs() {
|
|||||||
Port: in.Spec.Ports[i].Port,
|
Port: in.Spec.Ports[i].Port,
|
||||||
Protocol: Protocol(in.Spec.Ports[i].Protocol),
|
Protocol: Protocol(in.Spec.Ports[i].Protocol),
|
||||||
ContainerPort: in.Spec.Ports[i].TargetPort,
|
ContainerPort: in.Spec.Ports[i].TargetPort,
|
||||||
|
NodePort: in.Spec.Ports[i].NodePort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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.PublicIPs = in.Spec.DeprecatedPublicIPs
|
||||||
out.PublicIPs = in.Spec.PublicIPs
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.Convert(&in.Status.LoadBalancer, &out.LoadBalancerStatus, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Convert(&in.Spec.Type, &out.Type, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.CreateExternalLoadBalancer = in.Spec.Type == api.ServiceTypeLoadBalancer
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
func(in *Service, out *api.Service, s conversion.Scope) error {
|
func(in *Service, out *api.Service, s conversion.Scope) error {
|
||||||
@@ -816,6 +825,7 @@ func addConversionFuncs() {
|
|||||||
Port: in.Ports[i].Port,
|
Port: in.Ports[i].Port,
|
||||||
Protocol: api.Protocol(in.Ports[i].Protocol),
|
Protocol: api.Protocol(in.Ports[i].Protocol),
|
||||||
TargetPort: in.Ports[i].ContainerPort,
|
TargetPort: in.Ports[i].ContainerPort,
|
||||||
|
NodePort: in.Ports[i].NodePort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -823,13 +833,28 @@ func addConversionFuncs() {
|
|||||||
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.DeprecatedPublicIPs = in.PublicIPs
|
||||||
out.Spec.PublicIPs = in.PublicIPs
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.Convert(&in.LoadBalancerStatus, &out.Status.LoadBalancer, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
typeIn := in.Type
|
||||||
|
if typeIn == "" {
|
||||||
|
if in.CreateExternalLoadBalancer {
|
||||||
|
typeIn = ServiceTypeLoadBalancer
|
||||||
|
} else {
|
||||||
|
typeIn = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Convert(&typeIn, &out.Spec.Type, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ func addDefaultingFuncs() {
|
|||||||
if obj.SessionAffinity == "" {
|
if obj.SessionAffinity == "" {
|
||||||
obj.SessionAffinity = ServiceAffinityNone
|
obj.SessionAffinity = ServiceAffinityNone
|
||||||
}
|
}
|
||||||
|
if obj.Type == "" {
|
||||||
|
if obj.CreateExternalLoadBalancer {
|
||||||
|
obj.Type = ServiceTypeLoadBalancer
|
||||||
|
} else {
|
||||||
|
obj.Type = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
} else if obj.Type == ServiceTypeLoadBalancer {
|
||||||
|
obj.CreateExternalLoadBalancer = true
|
||||||
|
}
|
||||||
for i := range obj.Ports {
|
for i := range obj.Ports {
|
||||||
sp := &obj.Ports[i]
|
sp := &obj.Ports[i]
|
||||||
if sp.Protocol == "" {
|
if sp.Protocol == "" {
|
||||||
|
|||||||
@@ -150,7 +150,20 @@ func TestSetDefaultService(t *testing.T) {
|
|||||||
t.Errorf("Expected default protocol :%s, got: %s", versioned.ProtocolTCP, svc2.Protocol)
|
t.Errorf("Expected default protocol :%s, got: %s", versioned.ProtocolTCP, svc2.Protocol)
|
||||||
}
|
}
|
||||||
if svc2.SessionAffinity != versioned.ServiceAffinityNone {
|
if svc2.SessionAffinity != versioned.ServiceAffinityNone {
|
||||||
t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity)
|
t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity)
|
||||||
|
}
|
||||||
|
if svc2.Type != versioned.ServiceTypeClusterIP {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetDefaultServiceWithLoadbalancer(t *testing.T) {
|
||||||
|
svc := &versioned.Service{}
|
||||||
|
svc.CreateExternalLoadBalancer = true
|
||||||
|
obj2 := roundTrip(t, runtime.Object(svc))
|
||||||
|
svc2 := obj2.(*versioned.Service)
|
||||||
|
if svc2.Type != versioned.ServiceTypeLoadBalancer {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeLoadBalancer, svc2.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -835,6 +835,24 @@ const (
|
|||||||
ServiceAffinityNone ServiceAffinity = "None"
|
ServiceAffinityNone ServiceAffinity = "None"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service Type string describes ingress methods for a service
|
||||||
|
type ServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ServiceTypeClusterIP means a service will only be accessible inside the
|
||||||
|
// cluster, via the portal IP.
|
||||||
|
ServiceTypeClusterIP ServiceType = "ClusterIP"
|
||||||
|
|
||||||
|
// ServiceTypeNodePort means a service will be exposed on one port of
|
||||||
|
// every node, in addition to 'ClusterIP' type.
|
||||||
|
ServiceTypeNodePort ServiceType = "NodePort"
|
||||||
|
|
||||||
|
// ServiceTypeLoadBalancer means a service will be exposed via an
|
||||||
|
// external load balancer (if the cloud provider supports it), in addition
|
||||||
|
// to 'NodePort' type.
|
||||||
|
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PortalIPNone - do not assign a portal IP
|
// PortalIPNone - do not assign a portal IP
|
||||||
// no proxying required and no environment variables should be created for pods
|
// no proxying required and no environment variables should be created for pods
|
||||||
@@ -873,9 +891,12 @@ type Service struct {
|
|||||||
// 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"`
|
||||||
|
|
||||||
// PublicIPs are used by external load balancers, or can be set by
|
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
|
||||||
|
Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"`
|
||||||
|
|
||||||
|
// Deprecated. PublicIPs are used by external load balancers, or can be set by
|
||||||
// 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:"deprecated. externally visible IPs (e.g. load balancers) that should be proxied to this service"`
|
||||||
|
|
||||||
// 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
|
||||||
@@ -896,6 +917,29 @@ type Service struct {
|
|||||||
// array. If this field is not specified, it will be populated from
|
// array. If this field is not specified, it will be populated from
|
||||||
// the legacy fields.
|
// the legacy fields.
|
||||||
Ports []ServicePort `json:"ports" description:"ports to be exposed 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; 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"`
|
||||||
|
|
||||||
|
// LoadBalancer contains the current status of the load-balancer,
|
||||||
|
// if one is present.
|
||||||
|
LoadBalancerStatus LoadBalancerStatus `json:"loadBalancerStatus,omitempty" description:"status of load-balancer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerStatus represents the status of a load-balancer
|
||||||
|
type LoadBalancerStatus struct {
|
||||||
|
// Ingress is a list containing ingress points for the load-balancer;
|
||||||
|
// traffic intended for the service should be sent to these ingress points.
|
||||||
|
Ingress []LoadBalancerIngress `json:"ingress,omitempty" description:"load-balancer ingress points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerIngress represents the status of a load-balancer ingress point:
|
||||||
|
// traffic intended for the service should be sent to an ingress point.
|
||||||
|
type LoadBalancerIngress struct {
|
||||||
|
// IP is set for load-balancer ingress points that are IP based
|
||||||
|
// (typically GCE or OpenStack load-balancers)
|
||||||
|
IP string `json:"ip,omitempty" description:"IP address of ingress point"`
|
||||||
|
|
||||||
|
// Hostname is set for load-balancer ingress points that are DNS based
|
||||||
|
// (typically AWS load-balancers)
|
||||||
|
Hostname string `json:"hostname,omitempty" description:"hostname of ingress point"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServicePort struct {
|
type ServicePort struct {
|
||||||
@@ -918,6 +962,10 @@ type ServicePort struct {
|
|||||||
// of Port is used (an identity map) - note this is a different default
|
// of Port is used (an identity map) - note this is a different default
|
||||||
// than Service.ContainerPort.
|
// 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"`
|
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"`
|
||||||
|
|
||||||
|
// The port on each node on which this service is exposed.
|
||||||
|
// Default is to auto-allocate a port if the ServiceType of this Service requires one.
|
||||||
|
NodePort int `json:"nodePort" description:"the port on each node on which this service is exposed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceAccount binds together:
|
// ServiceAccount binds together:
|
||||||
|
|||||||
@@ -696,19 +696,28 @@ func addConversionFuncs() {
|
|||||||
Port: in.Spec.Ports[i].Port,
|
Port: in.Spec.Ports[i].Port,
|
||||||
Protocol: Protocol(in.Spec.Ports[i].Protocol),
|
Protocol: Protocol(in.Spec.Ports[i].Protocol),
|
||||||
ContainerPort: in.Spec.Ports[i].TargetPort,
|
ContainerPort: in.Spec.Ports[i].TargetPort,
|
||||||
|
NodePort: in.Spec.Ports[i].NodePort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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.PublicIPs = in.Spec.DeprecatedPublicIPs
|
||||||
out.PublicIPs = in.Spec.PublicIPs
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.Convert(&in.Status.LoadBalancer, &out.LoadBalancerStatus, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.Convert(&in.Spec.Type, &out.Type, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.CreateExternalLoadBalancer = in.Spec.Type == api.ServiceTypeLoadBalancer
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
func(in *Service, out *api.Service, s conversion.Scope) error {
|
func(in *Service, out *api.Service, s conversion.Scope) error {
|
||||||
@@ -738,6 +747,7 @@ func addConversionFuncs() {
|
|||||||
Port: in.Ports[i].Port,
|
Port: in.Ports[i].Port,
|
||||||
Protocol: api.Protocol(in.Ports[i].Protocol),
|
Protocol: api.Protocol(in.Ports[i].Protocol),
|
||||||
TargetPort: in.Ports[i].ContainerPort,
|
TargetPort: in.Ports[i].ContainerPort,
|
||||||
|
NodePort: in.Ports[i].NodePort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -745,13 +755,28 @@ func addConversionFuncs() {
|
|||||||
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.DeprecatedPublicIPs = in.PublicIPs
|
||||||
out.Spec.PublicIPs = in.PublicIPs
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.Convert(&in.LoadBalancerStatus, &out.Status.LoadBalancer, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
typeIn := in.Type
|
||||||
|
if typeIn == "" {
|
||||||
|
if in.CreateExternalLoadBalancer {
|
||||||
|
typeIn = ServiceTypeLoadBalancer
|
||||||
|
} else {
|
||||||
|
typeIn = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Convert(&typeIn, &out.Spec.Type, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ func addDefaultingFuncs() {
|
|||||||
if obj.SessionAffinity == "" {
|
if obj.SessionAffinity == "" {
|
||||||
obj.SessionAffinity = ServiceAffinityNone
|
obj.SessionAffinity = ServiceAffinityNone
|
||||||
}
|
}
|
||||||
|
if obj.Type == "" {
|
||||||
|
if obj.CreateExternalLoadBalancer {
|
||||||
|
obj.Type = ServiceTypeLoadBalancer
|
||||||
|
} else {
|
||||||
|
obj.Type = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
} else if obj.Type == ServiceTypeLoadBalancer {
|
||||||
|
obj.CreateExternalLoadBalancer = true
|
||||||
|
}
|
||||||
for i := range obj.Ports {
|
for i := range obj.Ports {
|
||||||
sp := &obj.Ports[i]
|
sp := &obj.Ports[i]
|
||||||
if sp.Protocol == "" {
|
if sp.Protocol == "" {
|
||||||
|
|||||||
@@ -150,7 +150,20 @@ func TestSetDefaultService(t *testing.T) {
|
|||||||
t.Errorf("Expected default protocol :%s, got: %s", versioned.ProtocolTCP, svc2.Protocol)
|
t.Errorf("Expected default protocol :%s, got: %s", versioned.ProtocolTCP, svc2.Protocol)
|
||||||
}
|
}
|
||||||
if svc2.SessionAffinity != versioned.ServiceAffinityNone {
|
if svc2.SessionAffinity != versioned.ServiceAffinityNone {
|
||||||
t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity)
|
t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity)
|
||||||
|
}
|
||||||
|
if svc2.Type != versioned.ServiceTypeClusterIP {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetDefaultServiceWithLoadbalancer(t *testing.T) {
|
||||||
|
svc := &versioned.Service{}
|
||||||
|
svc.CreateExternalLoadBalancer = true
|
||||||
|
obj2 := roundTrip(t, runtime.Object(svc))
|
||||||
|
svc2 := obj2.(*versioned.Service)
|
||||||
|
if svc2.Type != versioned.ServiceTypeLoadBalancer {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeLoadBalancer, svc2.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -837,6 +837,24 @@ const (
|
|||||||
ServiceAffinityNone ServiceAffinity = "None"
|
ServiceAffinityNone ServiceAffinity = "None"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service Type string describes ingress methods for a service
|
||||||
|
type ServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ServiceTypeClusterIP means a service will only be accessible inside the
|
||||||
|
// cluster, via the portal IP.
|
||||||
|
ServiceTypeClusterIP ServiceType = "ClusterIP"
|
||||||
|
|
||||||
|
// ServiceTypeNodePort means a service will be exposed on one port of
|
||||||
|
// every node, in addition to 'ClusterIP' type.
|
||||||
|
ServiceTypeNodePort ServiceType = "NodePort"
|
||||||
|
|
||||||
|
// ServiceTypeLoadBalancer means a service will be exposed via an
|
||||||
|
// external load balancer (if the cloud provider supports it), in addition
|
||||||
|
// to 'NodePort' type.
|
||||||
|
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// PortalIPNone - do not assign a portal IP
|
// PortalIPNone - do not assign a portal IP
|
||||||
// no proxying required and no environment variables should be created for pods
|
// no proxying required and no environment variables should be created for pods
|
||||||
@@ -877,9 +895,12 @@ type Service struct {
|
|||||||
// 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"`
|
||||||
|
|
||||||
// PublicIPs are used by external load balancers, or can be set by
|
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
|
||||||
|
Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"`
|
||||||
|
|
||||||
|
// Deprecated. PublicIPs are used by external load balancers, or can be set by
|
||||||
// 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:"deprecated. externally visible IPs (e.g. load balancers) that should be proxied to this service"`
|
||||||
|
|
||||||
// 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
|
||||||
@@ -900,6 +921,29 @@ type Service struct {
|
|||||||
// array. If this field is not specified, it will be populated from
|
// array. If this field is not specified, it will be populated from
|
||||||
// the legacy fields.
|
// the legacy fields.
|
||||||
Ports []ServicePort `json:"ports" description:"ports to be exposed 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; 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"`
|
||||||
|
|
||||||
|
// LoadBalancer contains the current status of the load-balancer,
|
||||||
|
// if one is present.
|
||||||
|
LoadBalancerStatus LoadBalancerStatus `json:"loadBalancerStatus,omitempty" description:"status of load-balancer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerStatus represents the status of a load-balancer
|
||||||
|
type LoadBalancerStatus struct {
|
||||||
|
// Ingress is a list containing ingress points for the load-balancer;
|
||||||
|
// traffic intended for the service should be sent to these ingress points.
|
||||||
|
Ingress []LoadBalancerIngress `json:"ingress,omitempty" description:"load-balancer ingress points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerIngress represents the status of a load-balancer ingress point:
|
||||||
|
// traffic intended for the service should be sent to an ingress point.
|
||||||
|
type LoadBalancerIngress struct {
|
||||||
|
// IP is set for load-balancer ingress points that are IP based
|
||||||
|
// (typically GCE or OpenStack load-balancers)
|
||||||
|
IP string `json:"ip,omitempty" description:"IP address of ingress point"`
|
||||||
|
|
||||||
|
// Hostname is set for load-balancer ingress points that are DNS based
|
||||||
|
// (typically AWS load-balancers)
|
||||||
|
Hostname string `json:"hostname,omitempty" description:"hostname of ingress point"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServicePort struct {
|
type ServicePort struct {
|
||||||
@@ -922,6 +966,10 @@ type ServicePort struct {
|
|||||||
// of Port is used (an identity map) - note this is a different default
|
// of Port is used (an identity map) - note this is a different default
|
||||||
// than Service.ContainerPort.
|
// 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"`
|
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"`
|
||||||
|
|
||||||
|
// The port on each node on which this service is exposed.
|
||||||
|
// Default is to auto-allocate a port if the ServiceType of this Service requires one.
|
||||||
|
NodePort int `json:"nodePort" description:"the port on each node on which this service is exposed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceAccount binds together:
|
// ServiceAccount binds together:
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ func addConversionFuncs() {
|
|||||||
err := api.Scheme.AddConversionFuncs(
|
err := api.Scheme.AddConversionFuncs(
|
||||||
convert_v1beta3_Container_To_api_Container,
|
convert_v1beta3_Container_To_api_Container,
|
||||||
convert_api_Container_To_v1beta3_Container,
|
convert_api_Container_To_v1beta3_Container,
|
||||||
|
convert_v1beta3_ServiceSpec_To_api_ServiceSpec,
|
||||||
|
convert_api_ServiceSpec_To_v1beta3_ServiceSpec,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If one of the conversion functions is malformed, detect it immediately.
|
// If one of the conversion functions is malformed, detect it immediately.
|
||||||
@@ -329,3 +331,92 @@ func convert_api_Container_To_v1beta3_Container(in *api.Container, out *Containe
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_v1beta3_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.ServiceSpec, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*ServiceSpec))(in)
|
||||||
|
}
|
||||||
|
if in.Ports != nil {
|
||||||
|
out.Ports = make([]api.ServicePort, len(in.Ports))
|
||||||
|
for i := range in.Ports {
|
||||||
|
if err := convert_v1beta3_ServicePort_To_api_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Ports = nil
|
||||||
|
}
|
||||||
|
if in.Selector != nil {
|
||||||
|
out.Selector = make(map[string]string)
|
||||||
|
for key, val := range in.Selector {
|
||||||
|
out.Selector[key] = val
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Selector = nil
|
||||||
|
}
|
||||||
|
out.PortalIP = in.PortalIP
|
||||||
|
|
||||||
|
typeIn := in.Type
|
||||||
|
if typeIn == "" {
|
||||||
|
if in.CreateExternalLoadBalancer {
|
||||||
|
typeIn = ServiceTypeLoadBalancer
|
||||||
|
} else {
|
||||||
|
typeIn = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := s.Convert(&typeIn, &out.Type, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.PublicIPs != nil {
|
||||||
|
out.DeprecatedPublicIPs = make([]string, len(in.PublicIPs))
|
||||||
|
for i := range in.PublicIPs {
|
||||||
|
out.DeprecatedPublicIPs[i] = in.PublicIPs[i]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.DeprecatedPublicIPs = nil
|
||||||
|
}
|
||||||
|
out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_api_ServiceSpec_To_v1beta3_ServiceSpec(in *api.ServiceSpec, out *ServiceSpec, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*api.ServiceSpec))(in)
|
||||||
|
}
|
||||||
|
if in.Ports != nil {
|
||||||
|
out.Ports = make([]ServicePort, len(in.Ports))
|
||||||
|
for i := range in.Ports {
|
||||||
|
if err := convert_api_ServicePort_To_v1beta3_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Ports = nil
|
||||||
|
}
|
||||||
|
if in.Selector != nil {
|
||||||
|
out.Selector = make(map[string]string)
|
||||||
|
for key, val := range in.Selector {
|
||||||
|
out.Selector[key] = val
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Selector = nil
|
||||||
|
}
|
||||||
|
out.PortalIP = in.PortalIP
|
||||||
|
|
||||||
|
if err := s.Convert(&in.Type, &out.Type, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out.CreateExternalLoadBalancer = in.Type == api.ServiceTypeLoadBalancer
|
||||||
|
|
||||||
|
if in.DeprecatedPublicIPs != nil {
|
||||||
|
out.PublicIPs = make([]string, len(in.DeprecatedPublicIPs))
|
||||||
|
for i := range in.DeprecatedPublicIPs {
|
||||||
|
out.PublicIPs[i] = in.DeprecatedPublicIPs[i]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.PublicIPs = nil
|
||||||
|
}
|
||||||
|
out.SessionAffinity = ServiceAffinity(in.SessionAffinity)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -719,6 +719,32 @@ func convert_api_ListOptions_To_v1beta3_ListOptions(in *api.ListOptions, out *Li
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_api_LoadBalancerIngress_To_v1beta3_LoadBalancerIngress(in *api.LoadBalancerIngress, out *LoadBalancerIngress, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*api.LoadBalancerIngress))(in)
|
||||||
|
}
|
||||||
|
out.IP = in.IP
|
||||||
|
out.Hostname = in.Hostname
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_api_LoadBalancerStatus_To_v1beta3_LoadBalancerStatus(in *api.LoadBalancerStatus, out *LoadBalancerStatus, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*api.LoadBalancerStatus))(in)
|
||||||
|
}
|
||||||
|
if in.Ingress != nil {
|
||||||
|
out.Ingress = make([]LoadBalancerIngress, len(in.Ingress))
|
||||||
|
for i := range in.Ingress {
|
||||||
|
if err := convert_api_LoadBalancerIngress_To_v1beta3_LoadBalancerIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Ingress = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference(in *api.LocalObjectReference, out *LocalObjectReference, s conversion.Scope) error {
|
func convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference(in *api.LocalObjectReference, out *LocalObjectReference, s conversion.Scope) error {
|
||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*api.LocalObjectReference))(in)
|
defaulting.(func(*api.LocalObjectReference))(in)
|
||||||
@@ -1984,42 +2010,7 @@ func convert_api_ServicePort_To_v1beta3_ServicePort(in *api.ServicePort, out *Se
|
|||||||
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
out.NodePort = in.NodePort
|
||||||
}
|
|
||||||
|
|
||||||
func convert_api_ServiceSpec_To_v1beta3_ServiceSpec(in *api.ServiceSpec, out *ServiceSpec, s conversion.Scope) error {
|
|
||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
|
||||||
defaulting.(func(*api.ServiceSpec))(in)
|
|
||||||
}
|
|
||||||
if in.Ports != nil {
|
|
||||||
out.Ports = make([]ServicePort, len(in.Ports))
|
|
||||||
for i := range in.Ports {
|
|
||||||
if err := convert_api_ServicePort_To_v1beta3_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.Ports = nil
|
|
||||||
}
|
|
||||||
if in.Selector != nil {
|
|
||||||
out.Selector = make(map[string]string)
|
|
||||||
for key, val := range in.Selector {
|
|
||||||
out.Selector[key] = val
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.Selector = nil
|
|
||||||
}
|
|
||||||
out.PortalIP = in.PortalIP
|
|
||||||
out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer
|
|
||||||
if in.PublicIPs != nil {
|
|
||||||
out.PublicIPs = make([]string, len(in.PublicIPs))
|
|
||||||
for i := range in.PublicIPs {
|
|
||||||
out.PublicIPs[i] = in.PublicIPs[i]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.PublicIPs = nil
|
|
||||||
}
|
|
||||||
out.SessionAffinity = ServiceAffinity(in.SessionAffinity)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2027,6 +2018,9 @@ func convert_api_ServiceStatus_To_v1beta3_ServiceStatus(in *api.ServiceStatus, o
|
|||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*api.ServiceStatus))(in)
|
defaulting.(func(*api.ServiceStatus))(in)
|
||||||
}
|
}
|
||||||
|
if err := convert_api_LoadBalancerStatus_To_v1beta3_LoadBalancerStatus(&in.LoadBalancer, &out.LoadBalancer, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2914,6 +2908,32 @@ func convert_v1beta3_ListOptions_To_api_ListOptions(in *ListOptions, out *api.Li
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convert_v1beta3_LoadBalancerIngress_To_api_LoadBalancerIngress(in *LoadBalancerIngress, out *api.LoadBalancerIngress, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*LoadBalancerIngress))(in)
|
||||||
|
}
|
||||||
|
out.IP = in.IP
|
||||||
|
out.Hostname = in.Hostname
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convert_v1beta3_LoadBalancerStatus_To_api_LoadBalancerStatus(in *LoadBalancerStatus, out *api.LoadBalancerStatus, s conversion.Scope) error {
|
||||||
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
|
defaulting.(func(*LoadBalancerStatus))(in)
|
||||||
|
}
|
||||||
|
if in.Ingress != nil {
|
||||||
|
out.Ingress = make([]api.LoadBalancerIngress, len(in.Ingress))
|
||||||
|
for i := range in.Ingress {
|
||||||
|
if err := convert_v1beta3_LoadBalancerIngress_To_api_LoadBalancerIngress(&in.Ingress[i], &out.Ingress[i], s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Ingress = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference(in *LocalObjectReference, out *api.LocalObjectReference, s conversion.Scope) error {
|
func convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference(in *LocalObjectReference, out *api.LocalObjectReference, s conversion.Scope) error {
|
||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*LocalObjectReference))(in)
|
defaulting.(func(*LocalObjectReference))(in)
|
||||||
@@ -4179,42 +4199,7 @@ func convert_v1beta3_ServicePort_To_api_ServicePort(in *ServicePort, out *api.Se
|
|||||||
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
if err := s.Convert(&in.TargetPort, &out.TargetPort, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
out.NodePort = in.NodePort
|
||||||
}
|
|
||||||
|
|
||||||
func convert_v1beta3_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.ServiceSpec, s conversion.Scope) error {
|
|
||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
|
||||||
defaulting.(func(*ServiceSpec))(in)
|
|
||||||
}
|
|
||||||
if in.Ports != nil {
|
|
||||||
out.Ports = make([]api.ServicePort, len(in.Ports))
|
|
||||||
for i := range in.Ports {
|
|
||||||
if err := convert_v1beta3_ServicePort_To_api_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.Ports = nil
|
|
||||||
}
|
|
||||||
if in.Selector != nil {
|
|
||||||
out.Selector = make(map[string]string)
|
|
||||||
for key, val := range in.Selector {
|
|
||||||
out.Selector[key] = val
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.Selector = nil
|
|
||||||
}
|
|
||||||
out.PortalIP = in.PortalIP
|
|
||||||
out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer
|
|
||||||
if in.PublicIPs != nil {
|
|
||||||
out.PublicIPs = make([]string, len(in.PublicIPs))
|
|
||||||
for i := range in.PublicIPs {
|
|
||||||
out.PublicIPs[i] = in.PublicIPs[i]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.PublicIPs = nil
|
|
||||||
}
|
|
||||||
out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4222,6 +4207,9 @@ func convert_v1beta3_ServiceStatus_To_api_ServiceStatus(in *ServiceStatus, out *
|
|||||||
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
|
||||||
defaulting.(func(*ServiceStatus))(in)
|
defaulting.(func(*ServiceStatus))(in)
|
||||||
}
|
}
|
||||||
|
if err := convert_v1beta3_LoadBalancerStatus_To_api_LoadBalancerStatus(&in.LoadBalancer, &out.LoadBalancer, s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4457,6 +4445,8 @@ func init() {
|
|||||||
convert_api_ListMeta_To_v1beta3_ListMeta,
|
convert_api_ListMeta_To_v1beta3_ListMeta,
|
||||||
convert_api_ListOptions_To_v1beta3_ListOptions,
|
convert_api_ListOptions_To_v1beta3_ListOptions,
|
||||||
convert_api_List_To_v1beta3_List,
|
convert_api_List_To_v1beta3_List,
|
||||||
|
convert_api_LoadBalancerIngress_To_v1beta3_LoadBalancerIngress,
|
||||||
|
convert_api_LoadBalancerStatus_To_v1beta3_LoadBalancerStatus,
|
||||||
convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference,
|
convert_api_LocalObjectReference_To_v1beta3_LocalObjectReference,
|
||||||
convert_api_NFSVolumeSource_To_v1beta3_NFSVolumeSource,
|
convert_api_NFSVolumeSource_To_v1beta3_NFSVolumeSource,
|
||||||
convert_api_NamespaceList_To_v1beta3_NamespaceList,
|
convert_api_NamespaceList_To_v1beta3_NamespaceList,
|
||||||
@@ -4517,7 +4507,6 @@ func init() {
|
|||||||
convert_api_ServiceAccount_To_v1beta3_ServiceAccount,
|
convert_api_ServiceAccount_To_v1beta3_ServiceAccount,
|
||||||
convert_api_ServiceList_To_v1beta3_ServiceList,
|
convert_api_ServiceList_To_v1beta3_ServiceList,
|
||||||
convert_api_ServicePort_To_v1beta3_ServicePort,
|
convert_api_ServicePort_To_v1beta3_ServicePort,
|
||||||
convert_api_ServiceSpec_To_v1beta3_ServiceSpec,
|
|
||||||
convert_api_ServiceStatus_To_v1beta3_ServiceStatus,
|
convert_api_ServiceStatus_To_v1beta3_ServiceStatus,
|
||||||
convert_api_Service_To_v1beta3_Service,
|
convert_api_Service_To_v1beta3_Service,
|
||||||
convert_api_StatusCause_To_v1beta3_StatusCause,
|
convert_api_StatusCause_To_v1beta3_StatusCause,
|
||||||
@@ -4568,6 +4557,8 @@ func init() {
|
|||||||
convert_v1beta3_ListMeta_To_api_ListMeta,
|
convert_v1beta3_ListMeta_To_api_ListMeta,
|
||||||
convert_v1beta3_ListOptions_To_api_ListOptions,
|
convert_v1beta3_ListOptions_To_api_ListOptions,
|
||||||
convert_v1beta3_List_To_api_List,
|
convert_v1beta3_List_To_api_List,
|
||||||
|
convert_v1beta3_LoadBalancerIngress_To_api_LoadBalancerIngress,
|
||||||
|
convert_v1beta3_LoadBalancerStatus_To_api_LoadBalancerStatus,
|
||||||
convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference,
|
convert_v1beta3_LocalObjectReference_To_api_LocalObjectReference,
|
||||||
convert_v1beta3_NFSVolumeSource_To_api_NFSVolumeSource,
|
convert_v1beta3_NFSVolumeSource_To_api_NFSVolumeSource,
|
||||||
convert_v1beta3_NamespaceList_To_api_NamespaceList,
|
convert_v1beta3_NamespaceList_To_api_NamespaceList,
|
||||||
@@ -4628,7 +4619,6 @@ func init() {
|
|||||||
convert_v1beta3_ServiceAccount_To_api_ServiceAccount,
|
convert_v1beta3_ServiceAccount_To_api_ServiceAccount,
|
||||||
convert_v1beta3_ServiceList_To_api_ServiceList,
|
convert_v1beta3_ServiceList_To_api_ServiceList,
|
||||||
convert_v1beta3_ServicePort_To_api_ServicePort,
|
convert_v1beta3_ServicePort_To_api_ServicePort,
|
||||||
convert_v1beta3_ServiceSpec_To_api_ServiceSpec,
|
|
||||||
convert_v1beta3_ServiceStatus_To_api_ServiceStatus,
|
convert_v1beta3_ServiceStatus_To_api_ServiceStatus,
|
||||||
convert_v1beta3_Service_To_api_Service,
|
convert_v1beta3_Service_To_api_Service,
|
||||||
convert_v1beta3_StatusCause_To_api_StatusCause,
|
convert_v1beta3_StatusCause_To_api_StatusCause,
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ func addDefaultingFuncs() {
|
|||||||
if obj.SessionAffinity == "" {
|
if obj.SessionAffinity == "" {
|
||||||
obj.SessionAffinity = ServiceAffinityNone
|
obj.SessionAffinity = ServiceAffinityNone
|
||||||
}
|
}
|
||||||
|
if obj.Type == "" {
|
||||||
|
if obj.CreateExternalLoadBalancer {
|
||||||
|
obj.Type = ServiceTypeLoadBalancer
|
||||||
|
} else {
|
||||||
|
obj.Type = ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
} else if obj.Type == ServiceTypeLoadBalancer {
|
||||||
|
obj.CreateExternalLoadBalancer = true
|
||||||
|
}
|
||||||
for i := range obj.Ports {
|
for i := range obj.Ports {
|
||||||
sp := &obj.Ports[i]
|
sp := &obj.Ports[i]
|
||||||
if sp.Protocol == "" {
|
if sp.Protocol == "" {
|
||||||
|
|||||||
@@ -160,7 +160,20 @@ func TestSetDefaultService(t *testing.T) {
|
|||||||
obj2 := roundTrip(t, runtime.Object(svc))
|
obj2 := roundTrip(t, runtime.Object(svc))
|
||||||
svc2 := obj2.(*versioned.Service)
|
svc2 := obj2.(*versioned.Service)
|
||||||
if svc2.Spec.SessionAffinity != versioned.ServiceAffinityNone {
|
if svc2.Spec.SessionAffinity != versioned.ServiceAffinityNone {
|
||||||
t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity)
|
t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity)
|
||||||
|
}
|
||||||
|
if svc2.Spec.Type != versioned.ServiceTypeClusterIP {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Spec.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetDefaultServiceWithLoadbalancer(t *testing.T) {
|
||||||
|
svc := &versioned.Service{}
|
||||||
|
svc.Spec.CreateExternalLoadBalancer = true
|
||||||
|
obj2 := roundTrip(t, runtime.Object(svc))
|
||||||
|
svc2 := obj2.(*versioned.Service)
|
||||||
|
if svc2.Spec.Type != versioned.ServiceTypeLoadBalancer {
|
||||||
|
t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeLoadBalancer, svc2.Spec.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -997,8 +997,49 @@ const (
|
|||||||
ServiceAffinityNone ServiceAffinity = "None"
|
ServiceAffinityNone ServiceAffinity = "None"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Service Type string describes ingress methods for a service
|
||||||
|
type ServiceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ServiceTypeClusterIP means a service will only be accessible inside the
|
||||||
|
// cluster, via the portal IP.
|
||||||
|
ServiceTypeClusterIP ServiceType = "ClusterIP"
|
||||||
|
|
||||||
|
// ServiceTypeNodePort means a service will be exposed on one port of
|
||||||
|
// every node, in addition to 'ClusterIP' type.
|
||||||
|
ServiceTypeNodePort ServiceType = "NodePort"
|
||||||
|
|
||||||
|
// ServiceTypeLoadBalancer means a service will be exposed via an
|
||||||
|
// external load balancer (if the cloud provider supports it), in addition
|
||||||
|
// to 'NodePort' type.
|
||||||
|
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"
|
||||||
|
)
|
||||||
|
|
||||||
// ServiceStatus represents the current status of a service
|
// ServiceStatus represents the current status of a service
|
||||||
type ServiceStatus struct{}
|
type ServiceStatus struct {
|
||||||
|
// LoadBalancer contains the current status of the load-balancer,
|
||||||
|
// if one is present.
|
||||||
|
LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty" description:"status of load-balancer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerStatus represents the status of a load-balancer
|
||||||
|
type LoadBalancerStatus struct {
|
||||||
|
// Ingress is a list containing ingress points for the load-balancer;
|
||||||
|
// traffic intended for the service should be sent to these ingress points.
|
||||||
|
Ingress []LoadBalancerIngress `json:"ingress,omitempty" description:"load-balancer ingress points"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadBalancerIngress represents the status of a load-balancer ingress point:
|
||||||
|
// traffic intended for the service should be sent to an ingress point.
|
||||||
|
type LoadBalancerIngress struct {
|
||||||
|
// IP is set for load-balancer ingress points that are IP based
|
||||||
|
// (typically GCE or OpenStack load-balancers)
|
||||||
|
IP string `json:"ip,omitempty" description:"IP address of ingress point"`
|
||||||
|
|
||||||
|
// Hostname is set for load-balancer ingress points that are DNS based
|
||||||
|
// (typically AWS load-balancers)
|
||||||
|
Hostname string `json:"hostname,omitempty" description:"hostname of ingress point"`
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
@@ -1018,9 +1059,12 @@ type ServiceSpec struct {
|
|||||||
// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.
|
// CreateExternalLoadBalancer indicates whether a load balancer should be created for this service.
|
||||||
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"`
|
||||||
|
|
||||||
// PublicIPs are used by external load balancers, or can be set by
|
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
|
||||||
|
Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"`
|
||||||
|
|
||||||
|
// Deprecated. PublicIPs are used by external load balancers, or can be set by
|
||||||
// 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:"deprecated. externally visible IPs (e.g. load balancers) that should be proxied to this service"`
|
||||||
|
|
||||||
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
|
// Optional: Supports "ClientIP" and "None". Used to maintain session affinity.
|
||||||
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
|
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None"`
|
||||||
@@ -1045,6 +1089,10 @@ type ServicePort struct {
|
|||||||
// target Pod's container ports. If this is not specified, the value
|
// target Pod's container ports. If this is not specified, the value
|
||||||
// of Port is used (an identity map).
|
// of Port is used (an identity map).
|
||||||
TargetPort util.IntOrString `json:"targetPort,omitempty" description:"the port to access on the pods targeted by the service; defaults to the service port"`
|
TargetPort util.IntOrString `json:"targetPort,omitempty" description:"the port to access on the pods targeted by the service; defaults to the service port"`
|
||||||
|
|
||||||
|
// The port on each node on which this service is exposed.
|
||||||
|
// Default is to auto-allocate a port if the ServiceType of this Service requires one.
|
||||||
|
NodePort int `json:"nodePort" description:"the port on each node on which this service is exposed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
@@ -1032,6 +1032,8 @@ func ValidatePodTemplateUpdate(newPod, oldPod *api.PodTemplate) errs.ValidationE
|
|||||||
}
|
}
|
||||||
|
|
||||||
var supportedSessionAffinityType = util.NewStringSet(string(api.ServiceAffinityClientIP), string(api.ServiceAffinityNone))
|
var supportedSessionAffinityType = util.NewStringSet(string(api.ServiceAffinityClientIP), string(api.ServiceAffinityNone))
|
||||||
|
var supportedServiceType = util.NewStringSet(string(api.ServiceTypeClusterIP), string(api.ServiceTypeNodePort),
|
||||||
|
string(api.ServiceTypeLoadBalancer))
|
||||||
|
|
||||||
// ValidateService tests if required fields in the service are set.
|
// ValidateService tests if required fields in the service are set.
|
||||||
func ValidateService(service *api.Service) errs.ValidationErrorList {
|
func ValidateService(service *api.Service) errs.ValidationErrorList {
|
||||||
@@ -1062,7 +1064,7 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip := range service.Spec.PublicIPs {
|
for _, ip := range service.Spec.DeprecatedPublicIPs {
|
||||||
if ip == "0.0.0.0" {
|
if ip == "0.0.0.0" {
|
||||||
allErrs = append(allErrs, errs.NewFieldInvalid("spec.publicIPs", ip, "is not an IP address"))
|
allErrs = append(allErrs, errs.NewFieldInvalid("spec.publicIPs", ip, "is not an IP address"))
|
||||||
} else if util.IsValidIPv4(ip) && net.ParseIP(ip).IsLoopback() {
|
} else if util.IsValidIPv4(ip) && net.ParseIP(ip).IsLoopback() {
|
||||||
@@ -1070,14 +1072,45 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if service.Spec.CreateExternalLoadBalancer {
|
if service.Spec.Type == "" {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldRequired("spec.type"))
|
||||||
|
} else if !supportedServiceType.Has(string(service.Spec.Type)) {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldNotSupported("spec.type", service.Spec.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Spec.Type == api.ServiceTypeLoadBalancer {
|
||||||
for i := range service.Spec.Ports {
|
for i := range service.Spec.Ports {
|
||||||
if service.Spec.Ports[i].Protocol != api.ProtocolTCP {
|
if service.Spec.Ports[i].Protocol != api.ProtocolTCP {
|
||||||
allErrs = append(allErrs, errs.NewFieldInvalid("spec.ports", service.Spec.Ports[i], "cannot create an external load balancer with non-TCP ports"))
|
allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.ports[%d].protocol", i), service.Spec.Ports[i].Protocol, "cannot create an external load balancer with non-TCP ports"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if service.Spec.Type == api.ServiceTypeClusterIP {
|
||||||
|
for i := range service.Spec.Ports {
|
||||||
|
if service.Spec.Ports[i].NodePort != 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.ports[%d].nodePort", i), service.Spec.Ports[i].NodePort, "cannot specify a node port with services of type ClusterIP"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate NodePorts, considering (protocol,port) pairs
|
||||||
|
nodePorts := make(map[api.ServicePort]bool)
|
||||||
|
for i := range service.Spec.Ports {
|
||||||
|
port := &service.Spec.Ports[i]
|
||||||
|
if port.NodePort == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var key api.ServicePort
|
||||||
|
key.Protocol = port.Protocol
|
||||||
|
key.NodePort = port.NodePort
|
||||||
|
_, found := nodePorts[key]
|
||||||
|
if found {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.ports[%d].nodePort", i), port.NodePort, "duplicate nodePort specified"))
|
||||||
|
}
|
||||||
|
nodePorts[key] = true
|
||||||
|
}
|
||||||
|
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1427,6 +1427,7 @@ func makeValidService() api.Service {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"key": "val"},
|
Selector: map[string]string{"key": "val"},
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}},
|
Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1522,6 +1523,13 @@ func TestValidateService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "missing type",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = ""
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "missing ports",
|
name: "missing ports",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
@@ -1603,21 +1611,21 @@ func TestValidateService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "invalid publicIPs localhost",
|
name: "invalid publicIPs localhost",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.PublicIPs = []string{"127.0.0.1"}
|
s.Spec.DeprecatedPublicIPs = []string{"127.0.0.1"}
|
||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid publicIPs",
|
name: "invalid publicIPs",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.PublicIPs = []string{"0.0.0.0"}
|
s.Spec.DeprecatedPublicIPs = []string{"0.0.0.0"}
|
||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid publicIPs host",
|
name: "valid publicIPs host",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.PublicIPs = []string{"myhost.mydomain"}
|
s.Spec.DeprecatedPublicIPs = []string{"myhost.mydomain"}
|
||||||
},
|
},
|
||||||
numErrs: 0,
|
numErrs: 0,
|
||||||
},
|
},
|
||||||
@@ -1632,7 +1640,7 @@ func TestValidateService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "invalid load balancer protocol 1",
|
name: "invalid load balancer protocol 1",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.CreateExternalLoadBalancer = true
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
s.Spec.Ports[0].Protocol = "UDP"
|
s.Spec.Ports[0].Protocol = "UDP"
|
||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
@@ -1640,7 +1648,7 @@ func TestValidateService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "invalid load balancer protocol 2",
|
name: "invalid load balancer protocol 2",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.CreateExternalLoadBalancer = true
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "UDP"})
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "UDP"})
|
||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
@@ -1683,16 +1691,135 @@ func TestValidateService(t *testing.T) {
|
|||||||
numErrs: 0,
|
numErrs: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid external load balancer",
|
name: "valid type - cluster",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.CreateExternalLoadBalancer = true
|
s.Spec.Type = api.ServiceTypeClusterIP
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type - loadbalancer",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type loadbalancer 2 ports",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
||||||
},
|
},
|
||||||
numErrs: 0,
|
numErrs: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "valid external load balancer 2 ports",
|
name: "valid external load balancer 2 ports",
|
||||||
tweakSvc: func(s *api.Service) {
|
tweakSvc: func(s *api.Service) {
|
||||||
s.Spec.CreateExternalLoadBalancer = true
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate nodeports",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1})
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "r", Port: 2, Protocol: "TCP", NodePort: 1})
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate nodeports (different protocols)",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 1, Protocol: "TCP", NodePort: 1})
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "r", Port: 2, Protocol: "UDP", NodePort: 1})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type - cluster",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeClusterIP
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type - nodeport",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type - loadbalancer",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type loadbalancer 2 ports",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type loadbalancer with NodePort",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type=NodePort service with NodePort",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type=NodePort service without NodePort",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid cluster service without NodePort",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeClusterIP
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid cluster service with NodePort",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeClusterIP
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP", NodePort: 12345})
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid public service with duplicate NodePort",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p1", Port: 1, Protocol: "TCP", NodePort: 1})
|
||||||
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "p2", Port: 2, Protocol: "TCP", NodePort: 1})
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid type=LoadBalancer",
|
||||||
|
tweakSvc: func(s *api.Service) {
|
||||||
|
s.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"})
|
||||||
},
|
},
|
||||||
numErrs: 0,
|
numErrs: 0,
|
||||||
@@ -2458,6 +2585,27 @@ func TestValidateServiceUpdate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
numErrs: 1,
|
numErrs: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "change type",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
||||||
|
newSvc.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove type",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
||||||
|
newSvc.Spec.Type = ""
|
||||||
|
},
|
||||||
|
numErrs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "change type -> nodeport",
|
||||||
|
tweakSvc: func(oldSvc, newSvc *api.Service) {
|
||||||
|
newSvc.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
},
|
||||||
|
numErrs: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ func GetLoadBalancerName(service *api.Service) string {
|
|||||||
type TCPLoadBalancer interface {
|
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
|
||||||
// GetTCPLoadBalancer returns whether the specified load balancer exists, and
|
// GetTCPLoadBalancer returns whether the specified load balancer exists, and
|
||||||
// if so, what its IP address or hostname is.
|
// if so, what its status is.
|
||||||
GetTCPLoadBalancer(name, region string) (endpoint string, exists bool, err error)
|
GetTCPLoadBalancer(name, region string) (status *api.LoadBalancerStatus, exists bool, err 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 status of the balancer
|
||||||
CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.ServiceAffinity) (string, error)
|
CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, 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
|
||||||
// EnsureTCPLoadBalancerDeleted deletes the specified load balancer if it
|
// EnsureTCPLoadBalancerDeleted deletes the specified load balancer if it
|
||||||
|
|||||||
@@ -103,16 +103,23 @@ func (f *FakeCloud) Routes() (cloudprovider.Routes, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTCPLoadBalancer is a stub implementation of TCPLoadBalancer.GetTCPLoadBalancer.
|
// GetTCPLoadBalancer is a stub implementation of TCPLoadBalancer.GetTCPLoadBalancer.
|
||||||
func (f *FakeCloud) GetTCPLoadBalancer(name, region string) (endpoint string, exists bool, err error) {
|
func (f *FakeCloud) GetTCPLoadBalancer(name, region string) (*api.LoadBalancerStatus, bool, error) {
|
||||||
return f.ExternalIP.String(), f.Exists, f.Err
|
status := &api.LoadBalancerStatus{}
|
||||||
|
status.Ingress = []api.LoadBalancerIngress{{IP: f.ExternalIP.String()}}
|
||||||
|
|
||||||
|
return status, f.Exists, f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, ports []int, hosts []string, affinityType api.ServiceAffinity) (string, error) {
|
func (f *FakeCloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, error) {
|
||||||
f.addCall("create")
|
f.addCall("create")
|
||||||
f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, ports, hosts})
|
f.Balancers = append(f.Balancers, FakeBalancer{name, region, externalIP, ports, hosts})
|
||||||
return f.ExternalIP.String(), f.Err
|
|
||||||
|
status := &api.LoadBalancerStatus{}
|
||||||
|
status.Ingress = []api.LoadBalancerIngress{{IP: f.ExternalIP.String()}}
|
||||||
|
|
||||||
|
return status, f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
// UpdateTCPLoadBalancer is a test-spy implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
||||||
|
|||||||
@@ -282,15 +282,18 @@ func (gce *GCECloud) waitForZoneOp(op *compute.Operation) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetTCPLoadBalancer is an implementation of TCPLoadBalancer.GetTCPLoadBalancer
|
// GetTCPLoadBalancer is an implementation of TCPLoadBalancer.GetTCPLoadBalancer
|
||||||
func (gce *GCECloud) GetTCPLoadBalancer(name, region string) (endpoint string, exists bool, err error) {
|
func (gce *GCECloud) GetTCPLoadBalancer(name, region string) (*api.LoadBalancerStatus, bool, error) {
|
||||||
fw, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
fwd, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return fw.IPAddress, true, nil
|
status := &api.LoadBalancerStatus{}
|
||||||
|
status.Ingress = []api.LoadBalancerIngress{{IP: fwd.IPAddress}}
|
||||||
|
|
||||||
|
return status, true, nil
|
||||||
}
|
}
|
||||||
if isHTTPErrorCode(err, http.StatusNotFound) {
|
if isHTTPErrorCode(err, http.StatusNotFound) {
|
||||||
return "", false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
return "", false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func isHTTPErrorCode(err error, code int) bool {
|
func isHTTPErrorCode(err error, code int) bool {
|
||||||
@@ -314,17 +317,17 @@ func translateAffinityType(affinityType api.ServiceAffinity) GCEAffinityType {
|
|||||||
// CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
|
// CreateTCPLoadBalancer is an implementation of TCPLoadBalancer.CreateTCPLoadBalancer.
|
||||||
// TODO(a-robinson): Don't just ignore specified IP addresses. Check if they're
|
// TODO(a-robinson): Don't just ignore specified IP addresses. Check if they're
|
||||||
// owned by the project and available to be used, and use them if they are.
|
// owned by the project and available to be used, and use them if they are.
|
||||||
func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.ServiceAffinity) (string, error) {
|
func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, error) {
|
||||||
err := gce.makeTargetPool(name, region, hosts, translateAffinityType(affinityType))
|
err := gce.makeTargetPool(name, region, hosts, translateAffinityType(affinityType))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isHTTPErrorCode(err, http.StatusConflict) {
|
if !isHTTPErrorCode(err, http.StatusConflict) {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
glog.Infof("Creating forwarding rule pointing at target pool that already exists: %v", err)
|
glog.Infof("Creating forwarding rule pointing at target pool that already exists: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ports) == 0 {
|
if len(ports) == 0 {
|
||||||
return "", fmt.Errorf("no ports specified for GCE load balancer")
|
return nil, fmt.Errorf("no ports specified for GCE load balancer")
|
||||||
}
|
}
|
||||||
minPort := 65536
|
minPort := 65536
|
||||||
maxPort := 0
|
maxPort := 0
|
||||||
@@ -344,19 +347,22 @@ func (gce *GCECloud) CreateTCPLoadBalancer(name, region string, externalIP net.I
|
|||||||
}
|
}
|
||||||
op, err := gce.service.ForwardingRules.Insert(gce.projectID, region, req).Do()
|
op, err := gce.service.ForwardingRules.Insert(gce.projectID, region, req).Do()
|
||||||
if err != nil && !isHTTPErrorCode(err, http.StatusConflict) {
|
if err != nil && !isHTTPErrorCode(err, http.StatusConflict) {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
if op != nil {
|
if op != nil {
|
||||||
err = gce.waitForRegionOp(op, region)
|
err = gce.waitForRegionOp(op, region)
|
||||||
if err != nil && !isHTTPErrorCode(err, http.StatusConflict) {
|
if err != nil && !isHTTPErrorCode(err, http.StatusConflict) {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fwd, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
fwd, err := gce.service.ForwardingRules.Get(gce.projectID, region, name).Do()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
return fwd.IPAddress, nil
|
|
||||||
|
status := &api.LoadBalancerStatus{}
|
||||||
|
status.Ingress = []api.LoadBalancerIngress{{IP: fwd.IPAddress}}
|
||||||
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateTCPLoadBalancer is an implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
// UpdateTCPLoadBalancer is an implementation of TCPLoadBalancer.UpdateTCPLoadBalancer.
|
||||||
|
|||||||
@@ -457,15 +457,19 @@ func getVipByName(client *gophercloud.ServiceClient, name string) (*vips.Virtual
|
|||||||
return &vipList[0], nil
|
return &vipList[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalancer) GetTCPLoadBalancer(name, region string) (endpoint string, exists bool, err error) {
|
func (lb *LoadBalancer) GetTCPLoadBalancer(name, region string) (*api.LoadBalancerStatus, bool, error) {
|
||||||
vip, err := getVipByName(lb.network, name)
|
vip, err := getVipByName(lb.network, name)
|
||||||
if err == ErrNotFound {
|
if err == ErrNotFound {
|
||||||
return "", false, nil
|
return nil, false, nil
|
||||||
}
|
}
|
||||||
if vip == nil {
|
if vip == nil {
|
||||||
return "", false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
return vip.Address, true, err
|
|
||||||
|
status := &api.LoadBalancerStatus{}
|
||||||
|
status.Ingress = []api.LoadBalancerIngress{{IP: vip.Address}}
|
||||||
|
|
||||||
|
return status, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: This code currently ignores 'region' and always creates a
|
// TODO: This code currently ignores 'region' and always creates a
|
||||||
@@ -473,11 +477,11 @@ func (lb *LoadBalancer) GetTCPLoadBalancer(name, region string) (endpoint string
|
|||||||
// 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, ports []int, hosts []string, affinity api.ServiceAffinity) (string, error) {
|
func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP net.IP, ports []int, hosts []string, affinity api.ServiceAffinity) (*api.LoadBalancerStatus, error) {
|
||||||
glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, ports, hosts, affinity)
|
glog.V(4).Infof("CreateTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, ports, hosts, affinity)
|
||||||
|
|
||||||
if len(ports) > 1 {
|
if len(ports) > 1 {
|
||||||
return "", fmt.Errorf("multiple ports are not yet supported in openstack load balancers")
|
return nil, fmt.Errorf("multiple ports are not yet supported in openstack load balancers")
|
||||||
}
|
}
|
||||||
|
|
||||||
var persistence *vips.SessionPersistence
|
var persistence *vips.SessionPersistence
|
||||||
@@ -487,7 +491,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
|
|||||||
case api.ServiceAffinityClientIP:
|
case api.ServiceAffinityClientIP:
|
||||||
persistence = &vips.SessionPersistence{Type: "SOURCE_IP"}
|
persistence = &vips.SessionPersistence{Type: "SOURCE_IP"}
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported load balancer affinity: %v", affinity)
|
return nil, fmt.Errorf("unsupported load balancer affinity: %v", affinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
lbmethod := lb.opts.LBMethod
|
lbmethod := lb.opts.LBMethod
|
||||||
@@ -501,13 +505,13 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
|
|||||||
LBMethod: lbmethod,
|
LBMethod: lbmethod,
|
||||||
}).Extract()
|
}).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
addr, err := getAddressByName(lb.compute, host)
|
addr, err := getAddressByName(lb.compute, host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = members.Create(lb.network, members.CreateOpts{
|
_, err = members.Create(lb.network, members.CreateOpts{
|
||||||
@@ -517,7 +521,7 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
|
|||||||
}).Extract()
|
}).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pools.Delete(lb.network, pool.ID)
|
pools.Delete(lb.network, pool.ID)
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,14 +535,14 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
|
|||||||
}).Extract()
|
}).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pools.Delete(lb.network, pool.ID)
|
pools.Delete(lb.network, pool.ID)
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = pools.AssociateMonitor(lb.network, pool.ID, mon.ID).Extract()
|
_, err = pools.AssociateMonitor(lb.network, pool.ID, mon.ID).Extract()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
monitors.Delete(lb.network, mon.ID)
|
monitors.Delete(lb.network, mon.ID)
|
||||||
pools.Delete(lb.network, pool.ID)
|
pools.Delete(lb.network, pool.ID)
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,10 +560,13 @@ func (lb *LoadBalancer) CreateTCPLoadBalancer(name, region string, externalIP ne
|
|||||||
monitors.Delete(lb.network, mon.ID)
|
monitors.Delete(lb.network, mon.ID)
|
||||||
}
|
}
|
||||||
pools.Delete(lb.network, pool.ID)
|
pools.Delete(lb.network, pool.ID)
|
||||||
return "", err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return vip.Address, nil
|
status := &api.LoadBalancerStatus{}
|
||||||
|
status.Ingress = []api.LoadBalancerIngress{{IP: vip.Address}}
|
||||||
|
|
||||||
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *LoadBalancer) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
func (lb *LoadBalancer) UpdateTCPLoadBalancer(name, region string, hosts []string) error {
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ func (s *ServiceController) createLoadBalancerIfNeeded(namespacedName types.Name
|
|||||||
if cachedService != nil {
|
if cachedService != nil {
|
||||||
// If the service already exists but needs to be updated, delete it so that
|
// If the service already exists but needs to be updated, delete it so that
|
||||||
// we can recreate it cleanly.
|
// we can recreate it cleanly.
|
||||||
if cachedService.Spec.CreateExternalLoadBalancer {
|
if wantsExternalLoadBalancer(cachedService) {
|
||||||
glog.Infof("Deleting existing load balancer for service %s that needs an updated load balancer.", namespacedName)
|
glog.Infof("Deleting existing load balancer for service %s that needs an updated load balancer.", namespacedName)
|
||||||
if err := s.balancer.EnsureTCPLoadBalancerDeleted(s.loadBalancerName(cachedService), s.zone.Region); err != nil {
|
if err := s.balancer.EnsureTCPLoadBalancerDeleted(s.loadBalancerName(cachedService), s.zone.Region); err != nil {
|
||||||
return err, retryable
|
return err, retryable
|
||||||
@@ -240,49 +240,49 @@ func (s *ServiceController) createLoadBalancerIfNeeded(namespacedName types.Name
|
|||||||
} else {
|
} else {
|
||||||
// If we don't have any cached memory of the load balancer, we have to ask
|
// If we don't have any cached memory of the load balancer, we have to ask
|
||||||
// the cloud provider for what it knows about it.
|
// the cloud provider for what it knows about it.
|
||||||
endpoint, exists, err := s.balancer.GetTCPLoadBalancer(s.loadBalancerName(service), s.zone.Region)
|
status, exists, err := s.balancer.GetTCPLoadBalancer(s.loadBalancerName(service), s.zone.Region)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error getting LB for service %s", namespacedName), retryable
|
return fmt.Errorf("Error getting LB for service %s: %v", namespacedName, err), retryable
|
||||||
}
|
}
|
||||||
if exists && stringSlicesEqual(service.Spec.PublicIPs, []string{endpoint}) {
|
if exists && api.LoadBalancerStatusEqual(status, &service.Status.LoadBalancer) {
|
||||||
// TODO: If we could read more of the spec (ports, affinityType) of the
|
glog.Infof("LB already exists with status %s for previously uncached service %s", status, namespacedName)
|
||||||
// existing load balancer, we could better determine if an update is
|
|
||||||
// necessary in more cases. For now, we optimistically assume that a
|
|
||||||
// matching IP suffices.
|
|
||||||
glog.Infof("LB already exists with endpoint %s for previously uncached service %s", endpoint, namespacedName)
|
|
||||||
return nil, notRetryable
|
return nil, notRetryable
|
||||||
} else if exists {
|
} else if exists {
|
||||||
glog.Infof("Deleting old LB for previously uncached service %s whose endpoint %s doesn't match the service's desired IPs %v",
|
glog.Infof("Deleting old LB for previously uncached service %s whose endpoint %s doesn't match the service's desired IPs %v",
|
||||||
namespacedName, endpoint, service.Spec.PublicIPs)
|
namespacedName, status, service.Spec.DeprecatedPublicIPs)
|
||||||
if err := s.balancer.EnsureTCPLoadBalancerDeleted(s.loadBalancerName(service), s.zone.Region); err != nil {
|
if err := s.balancer.EnsureTCPLoadBalancerDeleted(s.loadBalancerName(service), s.zone.Region); err != nil {
|
||||||
return err, retryable
|
return err, retryable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !service.Spec.CreateExternalLoadBalancer {
|
// Save the state so we can avoid a write if it doesn't change
|
||||||
glog.Infof("Not creating LB for service %s that doesn't want one.", namespacedName)
|
previousState := api.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer)
|
||||||
return nil, notRetryable
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if !wantsExternalLoadBalancer(service) {
|
||||||
|
glog.Infof("Not creating LB for service %s that doesn't want one.", namespacedName)
|
||||||
|
|
||||||
|
service.Status.LoadBalancer = api.LoadBalancerStatus{}
|
||||||
|
} else {
|
||||||
glog.V(2).Infof("Creating LB for service %s", namespacedName)
|
glog.V(2).Infof("Creating LB for service %s", namespacedName)
|
||||||
|
|
||||||
// The load balancer doesn't exist yet, so create it.
|
// The load balancer doesn't exist yet, so create it.
|
||||||
publicIPstring := fmt.Sprint(service.Spec.PublicIPs)
|
|
||||||
err := s.createExternalLoadBalancer(service)
|
err := s.createExternalLoadBalancer(service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create external load balancer for service %s: %v", namespacedName, err), retryable
|
return fmt.Errorf("failed to create external load balancer for service %s: %v", namespacedName, err), retryable
|
||||||
}
|
}
|
||||||
|
|
||||||
if publicIPstring == fmt.Sprint(service.Spec.PublicIPs) {
|
|
||||||
glog.Infof("Not persisting unchanged service to registry.")
|
|
||||||
return nil, notRetryable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If creating the load balancer succeeded, persist the updated service.
|
// Write the state if changed
|
||||||
if err = s.persistUpdate(service); err != nil {
|
// TODO: Be careful here ... what if there were other changes to the service?
|
||||||
return fmt.Errorf("Failed to persist updated publicIPs to apiserver, even after retries. Giving up: %v", err), notRetryable
|
if !api.LoadBalancerStatusEqual(previousState, &service.Status.LoadBalancer) {
|
||||||
|
if err := s.persistUpdate(service); err != nil {
|
||||||
|
return fmt.Errorf("Failed to persist updated status to apiserver, even after retries. Giving up: %v", err), notRetryable
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
glog.Infof("Not persisting unchanged LoadBalancerStatus to registry.")
|
||||||
|
}
|
||||||
|
|
||||||
return nil, notRetryable
|
return nil, notRetryable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,13 +301,13 @@ func (s *ServiceController) persistUpdate(service *api.Service) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// TODO: Try to resolve the conflict if the change was unrelated to load
|
// TODO: Try to resolve the conflict if the change was unrelated to load
|
||||||
// balancers and public IPs. For now, just rely on the fact that we'll
|
// balancer status. For now, just rely on the fact that we'll
|
||||||
// also process the update that caused the resource version to change.
|
// also process the update that caused the resource version to change.
|
||||||
if errors.IsConflict(err) {
|
if errors.IsConflict(err) {
|
||||||
glog.Infof("Not persisting update to service that has been changed since we received it: %v", err)
|
glog.Infof("Not persisting update to service that has been changed since we received it: %v", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
glog.Warningf("Failed to persist updated PublicIPs to service %s after creating its external load balancer: %v",
|
glog.Warningf("Failed to persist updated LoadBalancerStatus to service %s after creating its external load balancer: %v",
|
||||||
service.Name, err)
|
service.Name, err)
|
||||||
time.Sleep(clientRetryInterval)
|
time.Sleep(clientRetryInterval)
|
||||||
}
|
}
|
||||||
@@ -324,25 +324,27 @@ func (s *ServiceController) createExternalLoadBalancer(service *api.Service) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
name := s.loadBalancerName(service)
|
name := s.loadBalancerName(service)
|
||||||
if len(service.Spec.PublicIPs) > 0 {
|
if len(service.Spec.DeprecatedPublicIPs) > 0 {
|
||||||
for _, publicIP := range service.Spec.PublicIPs {
|
for _, publicIP := range service.Spec.DeprecatedPublicIPs {
|
||||||
// TODO: Make this actually work for multiple IPs by using different
|
// TODO: Make this actually work for multiple IPs by using different
|
||||||
// names for each. For now, we'll just create the first and break.
|
// names for each. For now, we'll just create the first and break.
|
||||||
endpoint, err := s.balancer.CreateTCPLoadBalancer(name, s.zone.Region, net.ParseIP(publicIP),
|
status, err := s.balancer.CreateTCPLoadBalancer(name, s.zone.Region, net.ParseIP(publicIP),
|
||||||
ports, hostsFromNodeList(nodes), service.Spec.SessionAffinity)
|
ports, hostsFromNodeList(nodes), service.Spec.SessionAffinity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
} else {
|
||||||
|
service.Status.LoadBalancer = *status
|
||||||
}
|
}
|
||||||
service.Spec.PublicIPs = []string{endpoint}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
endpoint, err := s.balancer.CreateTCPLoadBalancer(name, s.zone.Region, nil,
|
status, err := s.balancer.CreateTCPLoadBalancer(name, s.zone.Region, nil,
|
||||||
ports, hostsFromNodeList(nodes), service.Spec.SessionAffinity)
|
ports, hostsFromNodeList(nodes), service.Spec.SessionAffinity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
} else {
|
||||||
|
service.Status.LoadBalancer = *status
|
||||||
}
|
}
|
||||||
service.Spec.PublicIPs = []string{endpoint}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -402,20 +404,20 @@ func (s *serviceCache) delete(serviceName string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func needsUpdate(oldService *api.Service, newService *api.Service) bool {
|
func needsUpdate(oldService *api.Service, newService *api.Service) bool {
|
||||||
if !oldService.Spec.CreateExternalLoadBalancer && !newService.Spec.CreateExternalLoadBalancer {
|
if !wantsExternalLoadBalancer(oldService) && !wantsExternalLoadBalancer(newService) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if oldService.Spec.CreateExternalLoadBalancer != newService.Spec.CreateExternalLoadBalancer {
|
if wantsExternalLoadBalancer(oldService) != wantsExternalLoadBalancer(newService) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if !portsEqual(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity {
|
if !portsEqual(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if len(oldService.Spec.PublicIPs) != len(newService.Spec.PublicIPs) {
|
if len(oldService.Spec.DeprecatedPublicIPs) != len(newService.Spec.DeprecatedPublicIPs) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
for i := range oldService.Spec.PublicIPs {
|
for i := range oldService.Spec.DeprecatedPublicIPs {
|
||||||
if oldService.Spec.PublicIPs[i] != newService.Spec.PublicIPs[i] {
|
if oldService.Spec.DeprecatedPublicIPs[i] != newService.Spec.DeprecatedPublicIPs[i] {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,7 +561,7 @@ func (s *ServiceController) updateLoadBalancerHosts(services []*cachedService, h
|
|||||||
// Updates the external load balancer of a service, assuming we hold the mutex
|
// Updates the external load balancer of a service, assuming we hold the mutex
|
||||||
// associated with the service.
|
// associated with the service.
|
||||||
func (s *ServiceController) lockedUpdateLoadBalancerHosts(service *api.Service, hosts []string) error {
|
func (s *ServiceController) lockedUpdateLoadBalancerHosts(service *api.Service, hosts []string) error {
|
||||||
if !service.Spec.CreateExternalLoadBalancer {
|
if !wantsExternalLoadBalancer(service) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,3 +579,7 @@ func (s *ServiceController) lockedUpdateLoadBalancerHosts(service *api.Service,
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func wantsExternalLoadBalancer(service *api.Service) bool {
|
||||||
|
return service.Spec.Type == api.ServiceTypeLoadBalancer
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ import (
|
|||||||
|
|
||||||
const region = "us-central"
|
const region = "us-central"
|
||||||
|
|
||||||
func newService(name string, uid types.UID, external bool) *api.Service {
|
func newService(name string, uid types.UID, serviceType api.ServiceType) *api.Service {
|
||||||
return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: "namespace", UID: uid}, Spec: api.ServiceSpec{CreateExternalLoadBalancer: external}}
|
return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: "namespace", UID: uid}, Spec: api.ServiceSpec{Type: serviceType}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateExternalLoadBalancer(t *testing.T) {
|
func TestCreateExternalLoadBalancer(t *testing.T) {
|
||||||
@@ -45,7 +45,7 @@ func TestCreateExternalLoadBalancer(t *testing.T) {
|
|||||||
Namespace: "default",
|
Namespace: "default",
|
||||||
},
|
},
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
CreateExternalLoadBalancer: false,
|
Type: api.ServiceTypeClusterIP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
@@ -62,7 +62,7 @@ func TestCreateExternalLoadBalancer(t *testing.T) {
|
|||||||
Port: 80,
|
Port: 80,
|
||||||
Protocol: api.ProtocolUDP,
|
Protocol: api.ProtocolUDP,
|
||||||
}},
|
}},
|
||||||
CreateExternalLoadBalancer: true,
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: true,
|
expectErr: true,
|
||||||
@@ -79,7 +79,7 @@ func TestCreateExternalLoadBalancer(t *testing.T) {
|
|||||||
Port: 80,
|
Port: 80,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
}},
|
}},
|
||||||
CreateExternalLoadBalancer: true,
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectErr: false,
|
expectErr: false,
|
||||||
@@ -144,15 +144,15 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Services do not have external load balancers: no calls should be made.
|
// Services do not have external load balancers: no calls should be made.
|
||||||
services: []*api.Service{
|
services: []*api.Service{
|
||||||
newService("s0", "111", false),
|
newService("s0", "111", api.ServiceTypeClusterIP),
|
||||||
newService("s1", "222", false),
|
newService("s1", "222", api.ServiceTypeNodePort),
|
||||||
},
|
},
|
||||||
expectedUpdateCalls: nil,
|
expectedUpdateCalls: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Services does have an external load balancer: one call should be made.
|
// Services does have an external load balancer: one call should be made.
|
||||||
services: []*api.Service{
|
services: []*api.Service{
|
||||||
newService("s0", "333", true),
|
newService("s0", "333", api.ServiceTypeLoadBalancer),
|
||||||
},
|
},
|
||||||
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
||||||
{Name: "a333", Region: region, Hosts: []string{"node0", "node1", "node73"}},
|
{Name: "a333", Region: region, Hosts: []string{"node0", "node1", "node73"}},
|
||||||
@@ -161,9 +161,9 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Three services have an external load balancer: three calls.
|
// Three services have an external load balancer: three calls.
|
||||||
services: []*api.Service{
|
services: []*api.Service{
|
||||||
newService("s0", "444", true),
|
newService("s0", "444", api.ServiceTypeLoadBalancer),
|
||||||
newService("s1", "555", true),
|
newService("s1", "555", api.ServiceTypeLoadBalancer),
|
||||||
newService("s2", "666", true),
|
newService("s2", "666", api.ServiceTypeLoadBalancer),
|
||||||
},
|
},
|
||||||
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
||||||
{Name: "a444", Region: region, Hosts: []string{"node0", "node1", "node73"}},
|
{Name: "a444", Region: region, Hosts: []string{"node0", "node1", "node73"}},
|
||||||
@@ -174,10 +174,10 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Two services have an external load balancer and two don't: two calls.
|
// Two services have an external load balancer and two don't: two calls.
|
||||||
services: []*api.Service{
|
services: []*api.Service{
|
||||||
newService("s0", "777", false),
|
newService("s0", "777", api.ServiceTypeNodePort),
|
||||||
newService("s1", "888", true),
|
newService("s1", "888", api.ServiceTypeLoadBalancer),
|
||||||
newService("s3", "999", true),
|
newService("s3", "999", api.ServiceTypeLoadBalancer),
|
||||||
newService("s4", "123", false),
|
newService("s4", "123", api.ServiceTypeClusterIP),
|
||||||
},
|
},
|
||||||
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
||||||
{Name: "a888", Region: region, Hosts: []string{"node0", "node1", "node73"}},
|
{Name: "a888", Region: region, Hosts: []string{"node0", "node1", "node73"}},
|
||||||
@@ -187,7 +187,7 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// One service has an external load balancer and one is nil: one call.
|
// One service has an external load balancer and one is nil: one call.
|
||||||
services: []*api.Service{
|
services: []*api.Service{
|
||||||
newService("s0", "234", true),
|
newService("s0", "234", api.ServiceTypeLoadBalancer),
|
||||||
nil,
|
nil,
|
||||||
},
|
},
|
||||||
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{
|
||||||
|
|||||||
@@ -72,9 +72,14 @@ func RunClusterInfo(factory *cmdutil.Factory, out io.Writer, cmd *cobra.Command)
|
|||||||
services := r.Object.(*api.ServiceList).Items
|
services := r.Object.(*api.ServiceList).Items
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
var link string
|
var link string
|
||||||
if len(service.Spec.PublicIPs) > 0 {
|
if len(service.Status.LoadBalancer.Ingress) > 0 {
|
||||||
|
ingress := service.Status.LoadBalancer.Ingress[0]
|
||||||
|
ip := ingress.IP
|
||||||
|
if ip == "" {
|
||||||
|
ip = ingress.Hostname
|
||||||
|
}
|
||||||
for _, port := range service.Spec.Ports {
|
for _, port := range service.Spec.Ports {
|
||||||
link += "http://" + service.Spec.PublicIPs[0] + ":" + strconv.Itoa(port.Port) + " "
|
link += "http://" + ip + ":" + strconv.Itoa(port.Port) + " "
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
link = client.Host + "/api/v1beta3/proxy/namespaces/" + service.ObjectMeta.Namespace + "/services/" + service.ObjectMeta.Name
|
link = client.Host + "/api/v1beta3/proxy/namespaces/" + service.ObjectMeta.Namespace + "/services/" + service.ObjectMeta.Name
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream`
|
|||||||
|
|
||||||
func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command {
|
func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--create-external-load-balancer=bool]",
|
Use: "expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type]",
|
||||||
Short: "Take a replicated application and expose it as Kubernetes Service",
|
Short: "Take a replicated application and expose it as Kubernetes Service",
|
||||||
Long: expose_long,
|
Long: expose_long,
|
||||||
Example: expose_example,
|
Example: expose_example,
|
||||||
@@ -60,7 +60,8 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command {
|
|||||||
cmd.Flags().String("protocol", "TCP", "The network protocol for the service to be created. Default is 'tcp'.")
|
cmd.Flags().String("protocol", "TCP", "The network protocol for the service to be created. Default is 'tcp'.")
|
||||||
cmd.Flags().Int("port", -1, "The port that the service should serve on. Required.")
|
cmd.Flags().Int("port", -1, "The port that the service should serve on. Required.")
|
||||||
cmd.MarkFlagRequired("port")
|
cmd.MarkFlagRequired("port")
|
||||||
cmd.Flags().Bool("create-external-load-balancer", false, "If true, create an external load balancer for this service. Implementation is cloud provider dependent. Default is 'false'.")
|
cmd.Flags().String("type", "", "Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP' unless --create-external-load-balancer is specified.")
|
||||||
|
cmd.Flags().Bool("create-external-load-balancer", false, "If true, create an external load balancer for this service (trumped by --type). Implementation is cloud provider dependent. Default is 'false'.")
|
||||||
cmd.Flags().String("selector", "", "A label selector to use for this service. If empty (the default) infer the selector from the replication controller.")
|
cmd.Flags().String("selector", "", "A label selector to use for this service. If empty (the default) infer the selector from the replication controller.")
|
||||||
cmd.Flags().StringP("labels", "l", "", "Labels to apply to the service created by this call.")
|
cmd.Flags().StringP("labels", "l", "", "Labels to apply to the service created by this call.")
|
||||||
cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without creating it.")
|
cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without creating it.")
|
||||||
@@ -161,6 +162,9 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str
|
|||||||
}
|
}
|
||||||
params["labels"] = kubectl.MakeLabels(labels)
|
params["labels"] = kubectl.MakeLabels(labels)
|
||||||
}
|
}
|
||||||
|
if v := cmdutil.GetFlagString(cmd, "type"); v != "" {
|
||||||
|
params["type"] = v
|
||||||
|
}
|
||||||
err = kubectl.ValidateParams(names, params)
|
err = kubectl.ValidateParams(names, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ 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{
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ func TestMerge(t *testing.T) {
|
|||||||
expected: &api.Service{
|
expected: &api.Service{
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -157,6 +158,7 @@ func TestMerge(t *testing.T) {
|
|||||||
expected: &api.Service{
|
expected: &api.Service{
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Selector: map[string]string{
|
Selector: map[string]string{
|
||||||
"version": "v2",
|
"version": "v2",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package kubectl
|
package kubectl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -480,6 +481,22 @@ func (d *ServiceDescriber) Describe(namespace, name string) (string, error) {
|
|||||||
return describeService(service, endpoints, events)
|
return describeService(service, endpoints, events)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildIngressString(ingress []api.LoadBalancerIngress) string {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
for i := range ingress {
|
||||||
|
if i != 0 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
if ingress[i].IP != "" {
|
||||||
|
buffer.WriteString(ingress[i].IP)
|
||||||
|
} else {
|
||||||
|
buffer.WriteString(ingress[i].Hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buffer.String()
|
||||||
|
}
|
||||||
|
|
||||||
func describeService(service *api.Service, endpoints *api.Endpoints, events *api.EventList) (string, error) {
|
func describeService(service *api.Service, endpoints *api.Endpoints, events *api.EventList) (string, error) {
|
||||||
if endpoints == nil {
|
if endpoints == nil {
|
||||||
endpoints = &api.Endpoints{}
|
endpoints = &api.Endpoints{}
|
||||||
@@ -488,10 +505,11 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api
|
|||||||
fmt.Fprintf(out, "Name:\t%s\n", service.Name)
|
fmt.Fprintf(out, "Name:\t%s\n", service.Name)
|
||||||
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(service.Labels))
|
fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(service.Labels))
|
||||||
fmt.Fprintf(out, "Selector:\t%s\n", formatLabels(service.Spec.Selector))
|
fmt.Fprintf(out, "Selector:\t%s\n", formatLabels(service.Spec.Selector))
|
||||||
|
fmt.Fprintf(out, "Type:\t%s\n", service.Spec.Type)
|
||||||
fmt.Fprintf(out, "IP:\t%s\n", service.Spec.PortalIP)
|
fmt.Fprintf(out, "IP:\t%s\n", service.Spec.PortalIP)
|
||||||
if len(service.Spec.PublicIPs) > 0 {
|
if len(service.Status.LoadBalancer.Ingress) > 0 {
|
||||||
list := strings.Join(service.Spec.PublicIPs, ", ")
|
list := buildIngressString(service.Status.LoadBalancer.Ingress)
|
||||||
fmt.Fprintf(out, "Public IPs:\t%s\n", list)
|
fmt.Fprintf(out, "LoadBalancer Ingress:\t%s\n", list)
|
||||||
}
|
}
|
||||||
for i := range service.Spec.Ports {
|
for i := range service.Spec.Ports {
|
||||||
sp := &service.Spec.Ports[i]
|
sp := &service.Spec.Ports[i]
|
||||||
@@ -501,6 +519,9 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api
|
|||||||
name = "<unnamed>"
|
name = "<unnamed>"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(out, "Port:\t%s\t%d/%s\n", name, sp.Port, sp.Protocol)
|
fmt.Fprintf(out, "Port:\t%s\t%d/%s\n", name, sp.Port, sp.Protocol)
|
||||||
|
if sp.NodePort != 0 {
|
||||||
|
fmt.Fprintf(out, "NodePort:\t%s\t%d/%s\n", name, sp.Port, sp.Protocol)
|
||||||
|
}
|
||||||
fmt.Fprintf(out, "Endpoints:\t%s\t%s\n", name, formatEndpoints(endpoints, util.NewStringSet(sp.Name)))
|
fmt.Fprintf(out, "Endpoints:\t%s\t%s\n", name, formatEndpoints(endpoints, util.NewStringSet(sp.Name)))
|
||||||
}
|
}
|
||||||
fmt.Fprintf(out, "Session Affinity:\t%s\n", service.Spec.SessionAffinity)
|
fmt.Fprintf(out, "Session Affinity:\t%s\n", service.Spec.SessionAffinity)
|
||||||
|
|||||||
@@ -552,8 +552,12 @@ func printService(svc *api.Service, w io.Writer, withNamespace bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ips := []string{svc.Spec.PortalIP}
|
ips := []string{svc.Spec.PortalIP}
|
||||||
for _, publicIP := range svc.Spec.PublicIPs {
|
|
||||||
ips = append(ips, publicIP)
|
ingress := svc.Status.LoadBalancer.Ingress
|
||||||
|
for i := range ingress {
|
||||||
|
if ingress[i].IP != "" {
|
||||||
|
ips = append(ips, ingress[i].IP)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", name, formatLabels(svc.Labels),
|
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d/%s\n", name, formatLabels(svc.Labels),
|
||||||
|
|||||||
@@ -646,10 +646,6 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
PortalIP: "1.2.3.4",
|
PortalIP: "1.2.3.4",
|
||||||
PublicIPs: []string{
|
|
||||||
"2.3.4.5",
|
|
||||||
"3.4.5.6",
|
|
||||||
},
|
|
||||||
Ports: []api.ServicePort{
|
Ports: []api.ServicePort{
|
||||||
{
|
{
|
||||||
Port: 80,
|
Port: 80,
|
||||||
@@ -657,6 +653,18 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Status: api.ServiceStatus{
|
||||||
|
LoadBalancer: api.LoadBalancerStatus{
|
||||||
|
Ingress: []api.LoadBalancerIngress{
|
||||||
|
{
|
||||||
|
IP: "2.3.4.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "3.4.5.6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
@@ -680,9 +688,6 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
PortalIP: "1.2.3.4",
|
PortalIP: "1.2.3.4",
|
||||||
PublicIPs: []string{
|
|
||||||
"2.3.4.5",
|
|
||||||
},
|
|
||||||
Ports: []api.ServicePort{
|
Ports: []api.ServicePort{
|
||||||
{
|
{
|
||||||
Port: 80,
|
Port: 80,
|
||||||
@@ -698,15 +703,19 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Status: api.ServiceStatus{
|
||||||
|
LoadBalancer: api.LoadBalancerStatus{
|
||||||
|
Ingress: []api.LoadBalancerIngress{
|
||||||
|
{
|
||||||
|
IP: "2.3.4.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
PortalIP: "1.2.3.4",
|
PortalIP: "1.2.3.4",
|
||||||
PublicIPs: []string{
|
|
||||||
"2.3.4.5",
|
|
||||||
"4.5.6.7",
|
|
||||||
"5.6.7.8",
|
|
||||||
},
|
|
||||||
Ports: []api.ServicePort{
|
Ports: []api.ServicePort{
|
||||||
{
|
{
|
||||||
Port: 80,
|
Port: 80,
|
||||||
@@ -722,6 +731,22 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Status: api.ServiceStatus{
|
||||||
|
LoadBalancer: api.LoadBalancerStatus{
|
||||||
|
Ingress: []api.LoadBalancerIngress{
|
||||||
|
{
|
||||||
|
IP: "2.3.4.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "3.4.5.6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
IP: "5.6.7.8",
|
||||||
|
Hostname: "host5678",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,9 +759,10 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
t.Errorf("expected to contain portal ip %s, but doesn't: %s", ip, output)
|
t.Errorf("expected to contain portal ip %s, but doesn't: %s", ip, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ip = range svc.Spec.PublicIPs {
|
for _, ingress := range svc.Status.LoadBalancer.Ingress {
|
||||||
|
ip = ingress.IP
|
||||||
if !strings.Contains(output, ip) {
|
if !strings.Contains(output, ip) {
|
||||||
t.Errorf("expected to contain public ip %s, but doesn't: %s", ip, output)
|
t.Errorf("expected to contain ingress ip %s, but doesn't: %s", ip, output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,8 +774,8 @@ func TestPrintHumanReadableService(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// Max of # ports and (# public ip + portal ip)
|
// Max of # ports and (# public ip + portal ip)
|
||||||
count := len(svc.Spec.Ports)
|
count := len(svc.Spec.Ports)
|
||||||
if len(svc.Spec.PublicIPs)+1 > count {
|
if len(svc.Status.LoadBalancer.Ingress)+1 > count {
|
||||||
count = len(svc.Spec.PublicIPs) + 1
|
count = len(svc.Status.LoadBalancer.Ingress) + 1
|
||||||
}
|
}
|
||||||
if count != strings.Count(output, "\n") {
|
if count != strings.Count(output, "\n") {
|
||||||
t.Errorf("expected %d newlines, found %d", count, strings.Count(output, "\n"))
|
t.Errorf("expected %d newlines, found %d", count, strings.Count(output, "\n"))
|
||||||
@@ -907,9 +933,6 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) {
|
|||||||
ObjectMeta: api.ObjectMeta{Name: name, Namespace: namespaceName},
|
ObjectMeta: api.ObjectMeta{Name: name, Namespace: namespaceName},
|
||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
PortalIP: "1.2.3.4",
|
PortalIP: "1.2.3.4",
|
||||||
PublicIPs: []string{
|
|
||||||
"2.3.4.5",
|
|
||||||
},
|
|
||||||
Ports: []api.ServicePort{
|
Ports: []api.ServicePort{
|
||||||
{
|
{
|
||||||
Port: 80,
|
Port: 80,
|
||||||
@@ -917,6 +940,15 @@ func TestPrintHumanReadableWithNamespace(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Status: api.ServiceStatus{
|
||||||
|
LoadBalancer: api.LoadBalancerStatus{
|
||||||
|
Ingress: []api.LoadBalancerIngress{
|
||||||
|
{
|
||||||
|
IP: "2.3.4.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
printNamespace: true,
|
printNamespace: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func (ServiceGenerator) ParamNames() []GeneratorParam {
|
|||||||
{"labels", false},
|
{"labels", false},
|
||||||
{"public-ip", false},
|
{"public-ip", false},
|
||||||
{"create-external-load-balancer", false},
|
{"create-external-load-balancer", false},
|
||||||
|
{"type", false},
|
||||||
{"protocol", false},
|
{"protocol", false},
|
||||||
{"container-port", false}, // alias of target-port
|
{"container-port", false}, // alias of target-port
|
||||||
{"target-port", false},
|
{"target-port", false},
|
||||||
@@ -102,10 +103,13 @@ func (ServiceGenerator) Generate(params map[string]string) (runtime.Object, erro
|
|||||||
service.Spec.Ports[0].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.Type = api.ServiceTypeLoadBalancer
|
||||||
}
|
}
|
||||||
if len(params["public-ip"]) != 0 {
|
if len(params["public-ip"]) != 0 {
|
||||||
service.Spec.PublicIPs = []string{params["public-ip"]}
|
service.Spec.DeprecatedPublicIPs = []string{params["public-ip"]}
|
||||||
|
}
|
||||||
|
if len(params["type"]) != 0 {
|
||||||
|
service.Spec.Type = api.ServiceType(params["type"])
|
||||||
}
|
}
|
||||||
return &service, nil
|
return &service, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestGenerateService(t *testing.T) {
|
|||||||
TargetPort: util.NewIntOrStringFromString("foobar"),
|
TargetPort: util.NewIntOrStringFromString("foobar"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PublicIPs: []string{"1.2.3.4"},
|
DeprecatedPublicIPs: []string{"1.2.3.4"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -175,8 +175,69 @@ func TestGenerateService(t *testing.T) {
|
|||||||
TargetPort: util.NewIntOrStringFromString("foobar"),
|
TargetPort: util.NewIntOrStringFromString("foobar"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PublicIPs: []string{"1.2.3.4"},
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
CreateExternalLoadBalancer: true,
|
DeprecatedPublicIPs: []string{"1.2.3.4"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: map[string]string{
|
||||||
|
"selector": "foo=bar,baz=blah",
|
||||||
|
"name": "test",
|
||||||
|
"port": "80",
|
||||||
|
"protocol": "UDP",
|
||||||
|
"container-port": "foobar",
|
||||||
|
"type": string(api.ServiceTypeNodePort),
|
||||||
|
},
|
||||||
|
expected: api.Service{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
Spec: api.ServiceSpec{
|
||||||
|
Selector: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "blah",
|
||||||
|
},
|
||||||
|
Ports: []api.ServicePort{
|
||||||
|
{
|
||||||
|
Name: "default",
|
||||||
|
Port: 80,
|
||||||
|
Protocol: "UDP",
|
||||||
|
TargetPort: util.NewIntOrStringFromString("foobar"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Type: api.ServiceTypeNodePort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: map[string]string{
|
||||||
|
"selector": "foo=bar,baz=blah",
|
||||||
|
"name": "test",
|
||||||
|
"port": "80",
|
||||||
|
"protocol": "UDP",
|
||||||
|
"container-port": "foobar",
|
||||||
|
"create-external-load-balancer": "true", // ignored when type is present
|
||||||
|
"type": string(api.ServiceTypeNodePort),
|
||||||
|
},
|
||||||
|
expected: api.Service{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
},
|
||||||
|
Spec: api.ServiceSpec{
|
||||||
|
Selector: map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "blah",
|
||||||
|
},
|
||||||
|
Ports: []api.ServicePort{
|
||||||
|
{
|
||||||
|
Name: "default",
|
||||||
|
Port: 80,
|
||||||
|
Protocol: "UDP",
|
||||||
|
TargetPort: util.NewIntOrStringFromString("foobar"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Type: api.ServiceTypeNodePort,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/namespace"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
||||||
servicecontroller "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/controller"
|
servicecontroller "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/controller"
|
||||||
|
portallocatorcontroller "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/portallocator/controller"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
@@ -39,12 +40,16 @@ import (
|
|||||||
type Controller struct {
|
type Controller struct {
|
||||||
NamespaceRegistry namespace.Registry
|
NamespaceRegistry namespace.Registry
|
||||||
ServiceRegistry service.Registry
|
ServiceRegistry service.Registry
|
||||||
ServiceIPRegistry service.IPRegistry
|
ServiceIPRegistry service.RangeRegistry
|
||||||
EndpointRegistry endpoint.Registry
|
EndpointRegistry endpoint.Registry
|
||||||
PortalNet *net.IPNet
|
PortalNet *net.IPNet
|
||||||
// TODO: MasterCount is yucky
|
// TODO: MasterCount is yucky
|
||||||
MasterCount int
|
MasterCount int
|
||||||
|
|
||||||
|
ServiceNodePortRegistry service.RangeRegistry
|
||||||
|
ServiceNodePortInterval time.Duration
|
||||||
|
ServiceNodePorts util.PortRange
|
||||||
|
|
||||||
PortalIPInterval time.Duration
|
PortalIPInterval time.Duration
|
||||||
EndpointInterval time.Duration
|
EndpointInterval time.Duration
|
||||||
|
|
||||||
@@ -68,12 +73,16 @@ func (c *Controller) Start() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
repair := servicecontroller.NewRepair(c.PortalIPInterval, c.ServiceRegistry, c.PortalNet, c.ServiceIPRegistry)
|
repairPortals := servicecontroller.NewRepair(c.PortalIPInterval, c.ServiceRegistry, c.PortalNet, c.ServiceIPRegistry)
|
||||||
|
repairNodePorts := portallocatorcontroller.NewRepair(c.ServiceNodePortInterval, c.ServiceRegistry, c.ServiceNodePorts, c.ServiceNodePortRegistry)
|
||||||
|
|
||||||
// run all of the controllers once prior to returning from Start.
|
// run all of the controllers once prior to returning from Start.
|
||||||
if err := repair.RunOnce(); err != nil {
|
if err := repairPortals.RunOnce(); err != nil {
|
||||||
glog.Errorf("Unable to perform initial IP allocation check: %v", err)
|
glog.Errorf("Unable to perform initial IP allocation check: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := repairNodePorts.RunOnce(); err != nil {
|
||||||
|
glog.Errorf("Unable to perform initial service nodePort check: %v", err)
|
||||||
|
}
|
||||||
if err := c.UpdateKubernetesService(); err != nil {
|
if err := c.UpdateKubernetesService(); err != nil {
|
||||||
glog.Errorf("Unable to perform initial Kubernetes service initialization: %v", err)
|
glog.Errorf("Unable to perform initial Kubernetes service initialization: %v", err)
|
||||||
}
|
}
|
||||||
@@ -81,7 +90,7 @@ func (c *Controller) Start() {
|
|||||||
glog.Errorf("Unable to perform initial Kubernetes RO service initialization: %v", err)
|
glog.Errorf("Unable to perform initial Kubernetes RO service initialization: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.runner = util.NewRunner(c.RunKubernetesService, c.RunKubernetesROService, repair.RunUntil)
|
c.runner = util.NewRunner(c.RunKubernetesService, c.RunKubernetesROService, repairPortals.RunUntil, repairNodePorts.RunUntil)
|
||||||
c.runner.Start()
|
c.runner.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,13 +63,15 @@ import (
|
|||||||
resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd"
|
resourcequotaetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/resourcequota/etcd"
|
||||||
secretetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret/etcd"
|
secretetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/secret/etcd"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
||||||
|
etcdallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator/etcd"
|
||||||
ipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
ipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||||
etcdipallocator "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator/etcd"
|
|
||||||
serviceaccountetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount/etcd"
|
serviceaccountetcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/serviceaccount/etcd"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/ui"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/ui"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/portallocator"
|
||||||
"github.com/emicklei/go-restful"
|
"github.com/emicklei/go-restful"
|
||||||
"github.com/emicklei/go-restful/swagger"
|
"github.com/emicklei/go-restful/swagger"
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
@@ -141,12 +143,16 @@ type Config struct {
|
|||||||
|
|
||||||
// The name of the cluster.
|
// The name of the cluster.
|
||||||
ClusterName string
|
ClusterName string
|
||||||
|
|
||||||
|
// The range of ports to be assigned to services with type=NodePort or greater
|
||||||
|
ServiceNodePorts util.PortRange
|
||||||
}
|
}
|
||||||
|
|
||||||
// Master contains state for a Kubernetes cluster master/api server.
|
// Master contains state for a Kubernetes cluster master/api server.
|
||||||
type Master struct {
|
type Master struct {
|
||||||
// "Inputs", Copied from Config
|
// "Inputs", Copied from Config
|
||||||
portalNet *net.IPNet
|
portalNet *net.IPNet
|
||||||
|
serviceNodePorts util.PortRange
|
||||||
cacheTimeout time.Duration
|
cacheTimeout time.Duration
|
||||||
|
|
||||||
mux apiserver.Mux
|
mux apiserver.Mux
|
||||||
@@ -192,7 +198,8 @@ type Master struct {
|
|||||||
namespaceRegistry namespace.Registry
|
namespaceRegistry namespace.Registry
|
||||||
serviceRegistry service.Registry
|
serviceRegistry service.Registry
|
||||||
endpointRegistry endpoint.Registry
|
endpointRegistry endpoint.Registry
|
||||||
portalAllocator service.IPRegistry
|
portalAllocator service.RangeRegistry
|
||||||
|
serviceNodePortAllocator service.RangeRegistry
|
||||||
|
|
||||||
// "Outputs"
|
// "Outputs"
|
||||||
Handler http.Handler
|
Handler http.Handler
|
||||||
@@ -226,6 +233,15 @@ func setDefaults(c *Config) {
|
|||||||
}
|
}
|
||||||
c.PortalNet = portalNet
|
c.PortalNet = portalNet
|
||||||
}
|
}
|
||||||
|
if c.ServiceNodePorts.Size == 0 {
|
||||||
|
// TODO: Currently no way to specify an empty range (do we need to allow this?)
|
||||||
|
// We should probably allow this for clouds that don't require NodePort to do load-balancing (GCE)
|
||||||
|
// but then that breaks the strict nestedness of ServiceType.
|
||||||
|
// Review post-v1
|
||||||
|
defaultServiceNodePorts := util.PortRange{Base: 30000, Size: 2767}
|
||||||
|
c.ServiceNodePorts = defaultServiceNodePorts
|
||||||
|
glog.Infof("Node port range unspecified. Defaulting to %v.", c.ServiceNodePorts)
|
||||||
|
}
|
||||||
if c.MasterCount == 0 {
|
if c.MasterCount == 0 {
|
||||||
// Clearly, there will be at least one master.
|
// Clearly, there will be at least one master.
|
||||||
c.MasterCount = 1
|
c.MasterCount = 1
|
||||||
@@ -260,6 +276,7 @@ func setDefaults(c *Config) {
|
|||||||
// Certain config fields will be set to a default value if unset,
|
// Certain config fields will be set to a default value if unset,
|
||||||
// including:
|
// including:
|
||||||
// PortalNet
|
// PortalNet
|
||||||
|
// ServiceNodePorts
|
||||||
// MasterCount
|
// MasterCount
|
||||||
// ReadOnlyPort
|
// ReadOnlyPort
|
||||||
// ReadWritePort
|
// ReadWritePort
|
||||||
@@ -299,6 +316,7 @@ func New(c *Config) *Master {
|
|||||||
|
|
||||||
m := &Master{
|
m := &Master{
|
||||||
portalNet: c.PortalNet,
|
portalNet: c.PortalNet,
|
||||||
|
serviceNodePorts: c.ServiceNodePorts,
|
||||||
rootWebService: new(restful.WebService),
|
rootWebService: new(restful.WebService),
|
||||||
enableCoreControllers: c.EnableCoreControllers,
|
enableCoreControllers: c.EnableCoreControllers,
|
||||||
enableLogsSupport: c.EnableLogsSupport,
|
enableLogsSupport: c.EnableLogsSupport,
|
||||||
@@ -424,9 +442,23 @@ func (m *Master) init(c *Config) {
|
|||||||
registry := etcd.NewRegistry(c.EtcdHelper, podRegistry, m.endpointRegistry)
|
registry := etcd.NewRegistry(c.EtcdHelper, podRegistry, m.endpointRegistry)
|
||||||
m.serviceRegistry = registry
|
m.serviceRegistry = registry
|
||||||
|
|
||||||
ipAllocator := ipallocator.NewCIDRRange(m.portalNet)
|
var portalRangeRegistry service.RangeRegistry
|
||||||
portalAllocator := etcdipallocator.NewEtcd(ipAllocator, c.EtcdHelper)
|
portalAllocator := ipallocator.NewAllocatorCIDRRange(m.portalNet, func(max int, rangeSpec string) allocator.Interface {
|
||||||
m.portalAllocator = portalAllocator
|
mem := allocator.NewAllocationMap(max, rangeSpec)
|
||||||
|
etcd := etcdallocator.NewEtcd(mem, "/ranges/serviceips", "serviceipallocation", c.EtcdHelper)
|
||||||
|
portalRangeRegistry = etcd
|
||||||
|
return etcd
|
||||||
|
})
|
||||||
|
m.portalAllocator = portalRangeRegistry
|
||||||
|
|
||||||
|
var serviceNodePortRegistry service.RangeRegistry
|
||||||
|
serviceNodePortAllocator := portallocator.NewPortAllocatorCustom(m.serviceNodePorts, func(max int, rangeSpec string) allocator.Interface {
|
||||||
|
mem := allocator.NewAllocationMap(max, rangeSpec)
|
||||||
|
etcd := etcdallocator.NewEtcd(mem, "/ranges/servicenodeports", "servicenodeportallocation", c.EtcdHelper)
|
||||||
|
serviceNodePortRegistry = etcd
|
||||||
|
return etcd
|
||||||
|
})
|
||||||
|
m.serviceNodePortAllocator = serviceNodePortRegistry
|
||||||
|
|
||||||
controllerStorage := controlleretcd.NewREST(c.EtcdHelper)
|
controllerStorage := controlleretcd.NewREST(c.EtcdHelper)
|
||||||
|
|
||||||
@@ -444,7 +476,7 @@ func (m *Master) init(c *Config) {
|
|||||||
"podTemplates": podTemplateStorage,
|
"podTemplates": podTemplateStorage,
|
||||||
|
|
||||||
"replicationControllers": controllerStorage,
|
"replicationControllers": controllerStorage,
|
||||||
"services": service.NewStorage(m.serviceRegistry, m.nodeRegistry, m.endpointRegistry, portalAllocator, c.ClusterName),
|
"services": service.NewStorage(m.serviceRegistry, m.nodeRegistry, m.endpointRegistry, portalAllocator, serviceNodePortAllocator, c.ClusterName),
|
||||||
"endpoints": endpointsStorage,
|
"endpoints": endpointsStorage,
|
||||||
"minions": nodeStorage,
|
"minions": nodeStorage,
|
||||||
"minions/status": nodeStatusStorage,
|
"minions/status": nodeStatusStorage,
|
||||||
@@ -589,6 +621,10 @@ func (m *Master) NewBootstrapController() *Controller {
|
|||||||
PortalNet: m.portalNet,
|
PortalNet: m.portalNet,
|
||||||
MasterCount: m.masterCount,
|
MasterCount: m.masterCount,
|
||||||
|
|
||||||
|
ServiceNodePortRegistry: m.serviceNodePortAllocator,
|
||||||
|
ServiceNodePorts: m.serviceNodePorts,
|
||||||
|
|
||||||
|
ServiceNodePortInterval: 3 * time.Minute,
|
||||||
PortalIPInterval: 3 * time.Minute,
|
PortalIPInterval: 3 * time.Minute,
|
||||||
EndpointInterval: 10 * time.Second,
|
EndpointInterval: 10 * time.Second,
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ type serviceInfo struct {
|
|||||||
proxyPort int
|
proxyPort int
|
||||||
socket proxySocket
|
socket proxySocket
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
publicIPs []string // TODO: make this net.IP
|
nodePort int
|
||||||
|
loadBalancerStatus api.LoadBalancerStatus
|
||||||
sessionAffinityType api.ServiceAffinity
|
sessionAffinityType api.ServiceAffinity
|
||||||
stickyMaxAgeMinutes int
|
stickyMaxAgeMinutes int
|
||||||
}
|
}
|
||||||
@@ -61,12 +62,24 @@ type Proxier struct {
|
|||||||
loadBalancer LoadBalancer
|
loadBalancer LoadBalancer
|
||||||
mu sync.Mutex // protects serviceMap
|
mu sync.Mutex // protects serviceMap
|
||||||
serviceMap map[ServicePortName]*serviceInfo
|
serviceMap map[ServicePortName]*serviceInfo
|
||||||
|
portMapMutex sync.Mutex
|
||||||
|
portMap map[portMapKey]ServicePortName
|
||||||
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
|
||||||
hostIP net.IP
|
hostIP net.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A key for the portMap
|
||||||
|
type portMapKey struct {
|
||||||
|
port int
|
||||||
|
protocol api.Protocol
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *portMapKey) String() string {
|
||||||
|
return fmt.Sprintf("%s/%d", k.protocol, k.port)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrProxyOnLocalhost is returned by NewProxier if the user requests a proxier on
|
// ErrProxyOnLocalhost is returned by NewProxier if the user requests a proxier on
|
||||||
// the loopback address. May be checked for by callers of NewProxier to know whether
|
// the loopback address. May be checked for by callers of NewProxier to know whether
|
||||||
@@ -113,6 +126,7 @@ func createProxier(loadBalancer LoadBalancer, listenIP net.IP, iptables iptables
|
|||||||
return &Proxier{
|
return &Proxier{
|
||||||
loadBalancer: loadBalancer,
|
loadBalancer: loadBalancer,
|
||||||
serviceMap: make(map[ServicePortName]*serviceInfo),
|
serviceMap: make(map[ServicePortName]*serviceInfo),
|
||||||
|
portMap: make(map[portMapKey]ServicePortName),
|
||||||
listenIP: listenIP,
|
listenIP: listenIP,
|
||||||
iptables: iptables,
|
iptables: iptables,
|
||||||
hostIP: hostIP,
|
hostIP: hostIP,
|
||||||
@@ -273,7 +287,9 @@ func (proxier *Proxier) OnUpdate(services []api.Service) {
|
|||||||
}
|
}
|
||||||
info.portalIP = serviceIP
|
info.portalIP = serviceIP
|
||||||
info.portalPort = servicePort.Port
|
info.portalPort = servicePort.Port
|
||||||
info.publicIPs = service.Spec.PublicIPs
|
// Deep-copy in case the service instance changes
|
||||||
|
info.loadBalancerStatus = *api.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer)
|
||||||
|
info.nodePort = servicePort.NodePort
|
||||||
info.sessionAffinityType = service.Spec.SessionAffinity
|
info.sessionAffinityType = service.Spec.SessionAffinity
|
||||||
glog.V(4).Infof("info: %+v", info)
|
glog.V(4).Infof("info: %+v", info)
|
||||||
|
|
||||||
@@ -302,13 +318,13 @@ func (proxier *Proxier) OnUpdate(services []api.Service) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sameConfig(info *serviceInfo, service *api.Service, port *api.ServicePort) bool {
|
func sameConfig(info *serviceInfo, service *api.Service, port *api.ServicePort) bool {
|
||||||
if info.protocol != port.Protocol || info.portalPort != port.Port {
|
if info.protocol != port.Protocol || info.portalPort != port.Port || info.nodePort != port.NodePort {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !info.portalIP.Equal(net.ParseIP(service.Spec.PortalIP)) {
|
if !info.portalIP.Equal(net.ParseIP(service.Spec.PortalIP)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !ipsEqual(info.publicIPs, service.Spec.PublicIPs) {
|
if !api.LoadBalancerStatusEqual(&info.loadBalancerStatus, &service.Status.LoadBalancer) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if info.sessionAffinityType != service.Spec.SessionAffinity {
|
if info.sessionAffinityType != service.Spec.SessionAffinity {
|
||||||
@@ -334,8 +350,16 @@ func (proxier *Proxier) openPortal(service ServicePortName, info *serviceInfo) e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, publicIP := range info.publicIPs {
|
for _, ingress := range info.loadBalancerStatus.Ingress {
|
||||||
err = proxier.openOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)
|
if ingress.IP != "" {
|
||||||
|
err = proxier.openOnePortal(net.ParseIP(ingress.IP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.nodePort != 0 {
|
||||||
|
err = proxier.openNodePort(info.nodePort, info.protocol, proxier.listenIP, info.proxyPort, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -346,7 +370,7 @@ func (proxier *Proxier) openPortal(service ServicePortName, info *serviceInfo) e
|
|||||||
func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) 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.Append, iptables.TableNAT, iptablesContainerPortalChain, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Failed to install iptables %s rule for service %q", iptablesContainerPortalChain, name)
|
glog.Errorf("Failed to install iptables %s rule for service %q", iptablesContainerPortalChain, name)
|
||||||
return err
|
return err
|
||||||
@@ -357,7 +381,7 @@ func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol
|
|||||||
|
|
||||||
// Handle traffic from the host.
|
// Handle traffic from the host.
|
||||||
args = proxier.iptablesHostPortalArgs(portalIP, portalPort, protocol, proxyIP, proxyPort, name)
|
args = proxier.iptablesHostPortalArgs(portalIP, portalPort, protocol, proxyIP, proxyPort, name)
|
||||||
existed, err = proxier.iptables.EnsureRule(iptables.TableNAT, iptablesHostPortalChain, args...)
|
existed, err = proxier.iptables.EnsureRule(iptables.Append, iptables.TableNAT, iptablesHostPortalChain, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Errorf("Failed to install iptables %s rule for service %q", iptablesHostPortalChain, name)
|
glog.Errorf("Failed to install iptables %s rule for service %q", iptablesHostPortalChain, name)
|
||||||
return err
|
return err
|
||||||
@@ -368,11 +392,90 @@ func (proxier *Proxier) openOnePortal(portalIP net.IP, portalPort int, protocol
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marks a port as being owned by a particular service, or returns error if already claimed.
|
||||||
|
// Idempotent: reclaiming with the same owner is not an error
|
||||||
|
func (proxier *Proxier) claimPort(port int, protocol api.Protocol, owner ServicePortName) error {
|
||||||
|
proxier.portMapMutex.Lock()
|
||||||
|
defer proxier.portMapMutex.Unlock()
|
||||||
|
|
||||||
|
// TODO: We could pre-populate some reserved ports into portMap and/or blacklist some well-known ports
|
||||||
|
|
||||||
|
key := portMapKey{port: port, protocol: protocol}
|
||||||
|
existing, found := proxier.portMap[key]
|
||||||
|
if !found {
|
||||||
|
proxier.portMap[key] = owner
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if existing == owner {
|
||||||
|
// We are idempotent
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Port conflict detected on port %v. %v vs %v", key, owner, existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release a claim on a port. Returns an error if the owner does not match the claim.
|
||||||
|
// Tolerates release on an unclaimed port, to simplify .
|
||||||
|
func (proxier *Proxier) releasePort(port int, protocol api.Protocol, owner ServicePortName) error {
|
||||||
|
proxier.portMapMutex.Lock()
|
||||||
|
defer proxier.portMapMutex.Unlock()
|
||||||
|
|
||||||
|
key := portMapKey{port: port, protocol: protocol}
|
||||||
|
existing, found := proxier.portMap[key]
|
||||||
|
if !found {
|
||||||
|
// We tolerate this, it happens if we are cleaning up a failed allocation
|
||||||
|
glog.Infof("Ignoring release on unowned port: %v", key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if existing != owner {
|
||||||
|
return fmt.Errorf("Port conflict detected on port %v (unowned unlock). %v vs %v", key, owner, existing)
|
||||||
|
}
|
||||||
|
delete(proxier.portMap, key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (proxier *Proxier) openNodePort(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) error {
|
||||||
|
// TODO: Do we want to allow containers to access public services? Probably yes.
|
||||||
|
// TODO: We could refactor this to be the same code as portal, but with IP == nil
|
||||||
|
|
||||||
|
err := proxier.claimPort(nodePort, protocol, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle traffic from containers.
|
||||||
|
args := proxier.iptablesContainerNodePortArgs(nodePort, protocol, proxyIP, proxyPort, name)
|
||||||
|
existed, err := proxier.iptables.EnsureRule(iptables.Append, iptables.TableNAT, iptablesContainerNodePortChain, args...)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Failed to install iptables %s rule for service %q", iptablesContainerNodePortChain, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !existed {
|
||||||
|
glog.Infof("Opened iptables from-containers public port for service %q on %s port %d", name, protocol, nodePort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle traffic from the host.
|
||||||
|
args = proxier.iptablesHostNodePortArgs(nodePort, protocol, proxyIP, proxyPort, name)
|
||||||
|
existed, err = proxier.iptables.EnsureRule(iptables.Append, iptables.TableNAT, iptablesHostNodePortChain, args...)
|
||||||
|
if err != nil {
|
||||||
|
glog.Errorf("Failed to install iptables %s rule for service %q", iptablesHostNodePortChain, name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !existed {
|
||||||
|
glog.Infof("Opened iptables from-host public port for service %q on %s port %d", name, protocol, nodePort)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (proxier *Proxier) closePortal(service ServicePortName, 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.publicIPs {
|
for _, ingress := range info.loadBalancerStatus.Ingress {
|
||||||
el = append(el, proxier.closeOnePortal(net.ParseIP(publicIP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)...)
|
if ingress.IP != "" {
|
||||||
|
el = append(el, proxier.closeOnePortal(net.ParseIP(ingress.IP), info.portalPort, info.protocol, proxier.listenIP, info.proxyPort, service)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if info.nodePort != 0 {
|
||||||
|
el = append(el, proxier.closeNodePort(info.nodePort, info.protocol, proxier.listenIP, info.proxyPort, service)...)
|
||||||
}
|
}
|
||||||
if len(el) == 0 {
|
if len(el) == 0 {
|
||||||
glog.V(3).Infof("Closed iptables portals for service %q", service)
|
glog.V(3).Infof("Closed iptables portals for service %q", service)
|
||||||
@@ -402,28 +505,94 @@ func (proxier *Proxier) closeOnePortal(portalIP net.IP, portalPort int, protocol
|
|||||||
return el
|
return el
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (proxier *Proxier) closeNodePort(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, name ServicePortName) []error {
|
||||||
|
el := []error{}
|
||||||
|
|
||||||
|
// Handle traffic from containers.
|
||||||
|
args := proxier.iptablesContainerNodePortArgs(nodePort, protocol, proxyIP, proxyPort, name)
|
||||||
|
if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesContainerNodePortChain, args...); err != nil {
|
||||||
|
glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesContainerNodePortChain, name)
|
||||||
|
el = append(el, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle traffic from the host.
|
||||||
|
args = proxier.iptablesHostNodePortArgs(nodePort, protocol, proxyIP, proxyPort, name)
|
||||||
|
if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesHostNodePortChain, args...); err != nil {
|
||||||
|
glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesHostNodePortChain, name)
|
||||||
|
el = append(el, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proxier.releasePort(nodePort, protocol, name); err != nil {
|
||||||
|
el = append(el, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
// See comments in the *PortalArgs() functions for some details about why we
|
// See comments in the *PortalArgs() functions for some details about why we
|
||||||
// use two chains.
|
// use two chains for portals.
|
||||||
var iptablesContainerPortalChain iptables.Chain = "KUBE-PORTALS-CONTAINER"
|
var iptablesContainerPortalChain iptables.Chain = "KUBE-PORTALS-CONTAINER"
|
||||||
var iptablesHostPortalChain iptables.Chain = "KUBE-PORTALS-HOST"
|
var iptablesHostPortalChain iptables.Chain = "KUBE-PORTALS-HOST"
|
||||||
|
|
||||||
|
// Chains for NodePort services
|
||||||
|
var iptablesContainerNodePortChain iptables.Chain = "KUBE-NODEPORT-CONTAINER"
|
||||||
|
var iptablesHostNodePortChain iptables.Chain = "KUBE-NODEPORT-HOST"
|
||||||
|
|
||||||
// Ensure that the iptables infrastructure we use is set up. This can safely be called periodically.
|
// Ensure that the iptables infrastructure we use is set up. This can safely be called periodically.
|
||||||
func iptablesInit(ipt iptables.Interface) error {
|
func iptablesInit(ipt iptables.Interface) error {
|
||||||
// TODO: There is almost certainly room for optimization here. E.g. If
|
// TODO: There is almost certainly room for optimization here. E.g. If
|
||||||
// we knew the portal_net CIDR we could fast-track outbound packets not
|
// we knew the portal_net CIDR we could fast-track outbound packets not
|
||||||
// destined for a service. There's probably more, help wanted.
|
// destined for a service. There's probably more, help wanted.
|
||||||
|
|
||||||
|
// Danger - order of these rules matters here:
|
||||||
|
//
|
||||||
|
// We match portal rules first, then NodePort rules. For NodePort rules, we filter primarily on --dst-type LOCAL,
|
||||||
|
// because we want to listen on all local addresses, but don't match internet traffic with the same dst port number.
|
||||||
|
//
|
||||||
|
// There is one complication (per thockin):
|
||||||
|
// -m addrtype --dst-type LOCAL is what we want except that it is broken (by intent without foresight to our usecase)
|
||||||
|
// on at least GCE. Specifically, GCE machines have a daemon which learns what external IPs are forwarded to that
|
||||||
|
// machine, and configure a local route for that IP, making a match for --dst-type LOCAL when we don't want it to.
|
||||||
|
// Removing the route gives correct behavior until the daemon recreates it.
|
||||||
|
// Killing the daemon is an option, but means that any non-kubernetes use of the machine with external IP will be broken.
|
||||||
|
//
|
||||||
|
// This applies to IPs on GCE that are actually from a load-balancer; they will be categorized as LOCAL.
|
||||||
|
// _If_ the chains were in the wrong order, and the LB traffic had dst-port == a NodePort on some other service,
|
||||||
|
// the NodePort would take priority (incorrectly).
|
||||||
|
// This is unlikely (and would only affect outgoing traffic from the cluster to the load balancer, which seems
|
||||||
|
// doubly-unlikely), but we need to be careful to keep the rules in the right order.
|
||||||
|
args := []string{ /* portal_net matching could go here */ }
|
||||||
|
args = append(args, "-m", "comment", "--comment", "handle Portals; NOTE: this must be before the NodePort rules")
|
||||||
if _, err := ipt.EnsureChain(iptables.TableNAT, iptablesContainerPortalChain); err != nil {
|
if _, err := ipt.EnsureChain(iptables.TableNAT, iptablesContainerPortalChain); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := ipt.EnsureRule(iptables.TableNAT, iptables.ChainPrerouting, "-j", string(iptablesContainerPortalChain)); err != nil {
|
if _, err := ipt.EnsureRule(iptables.Prepend, iptables.TableNAT, iptables.ChainPrerouting, append(args, "-j", string(iptablesContainerPortalChain))...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := ipt.EnsureChain(iptables.TableNAT, iptablesHostPortalChain); err != nil {
|
if _, err := ipt.EnsureChain(iptables.TableNAT, iptablesHostPortalChain); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := ipt.EnsureRule(iptables.TableNAT, iptables.ChainOutput, "-j", string(iptablesHostPortalChain)); err != nil {
|
if _, err := ipt.EnsureRule(iptables.Prepend, iptables.TableNAT, iptables.ChainOutput, append(args, "-j", string(iptablesHostPortalChain))...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This set of rules matches broadly (addrtype & destination port), and therefore must come after the portal rules
|
||||||
|
args = []string{"-m", "addrtype", "--dst-type", "LOCAL"}
|
||||||
|
args = append(args, "-m", "comment", "--comment", "handle service NodePorts; NOTE: this must be the last rule in the chain")
|
||||||
|
if _, err := ipt.EnsureChain(iptables.TableNAT, iptablesContainerNodePortChain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := ipt.EnsureRule(iptables.Append, iptables.TableNAT, iptables.ChainPrerouting, append(args, "-j", string(iptablesContainerNodePortChain))...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := ipt.EnsureChain(iptables.TableNAT, iptablesHostNodePortChain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := ipt.EnsureRule(iptables.Append, iptables.TableNAT, iptables.ChainOutput, append(args, "-j", string(iptablesHostNodePortChain))...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Verify order of rules.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,6 +605,12 @@ func iptablesFlush(ipt iptables.Interface) error {
|
|||||||
if err := ipt.FlushChain(iptables.TableNAT, iptablesHostPortalChain); err != nil {
|
if err := ipt.FlushChain(iptables.TableNAT, iptablesHostPortalChain); err != nil {
|
||||||
el = append(el, err)
|
el = append(el, err)
|
||||||
}
|
}
|
||||||
|
if err := ipt.FlushChain(iptables.TableNAT, iptablesContainerNodePortChain); err != nil {
|
||||||
|
el = append(el, err)
|
||||||
|
}
|
||||||
|
if err := ipt.FlushChain(iptables.TableNAT, iptablesHostNodePortChain); err != nil {
|
||||||
|
el = append(el, err)
|
||||||
|
}
|
||||||
if len(el) != 0 {
|
if len(el) != 0 {
|
||||||
glog.Errorf("Some errors flushing old iptables portals: %v", el)
|
glog.Errorf("Some errors flushing old iptables portals: %v", el)
|
||||||
}
|
}
|
||||||
@@ -464,9 +639,13 @@ func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol
|
|||||||
"--comment", service.String(),
|
"--comment", service.String(),
|
||||||
"-p", strings.ToLower(string(protocol)),
|
"-p", strings.ToLower(string(protocol)),
|
||||||
"-m", strings.ToLower(string(protocol)),
|
"-m", strings.ToLower(string(protocol)),
|
||||||
"-d", fmt.Sprintf("%s/32", destIP.String()),
|
|
||||||
"--dport", fmt.Sprintf("%d", destPort),
|
"--dport", fmt.Sprintf("%d", destPort),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if destIP != nil {
|
||||||
|
args = append(args, "-d", fmt.Sprintf("%s/32", destIP.String()))
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +729,37 @@ func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, prot
|
|||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a slice of iptables args for a from-container public-port rule.
|
||||||
|
// See iptablesContainerPortalArgs
|
||||||
|
// TODO: Should we just reuse iptablesContainerPortalArgs?
|
||||||
|
func (proxier *Proxier) iptablesContainerNodePortArgs(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service ServicePortName) []string {
|
||||||
|
args := iptablesCommonPortalArgs(nil, nodePort, protocol, service)
|
||||||
|
|
||||||
|
if proxyIP.Equal(zeroIPv4) || proxyIP.Equal(zeroIPv6) {
|
||||||
|
// TODO: Can we REDIRECT with IPv6?
|
||||||
|
args = append(args, "-j", "REDIRECT", "--to-ports", fmt.Sprintf("%d", proxyPort))
|
||||||
|
} else {
|
||||||
|
// TODO: Can we DNAT with IPv6?
|
||||||
|
args = append(args, "-j", "DNAT", "--to-destination", net.JoinHostPort(proxyIP.String(), strconv.Itoa(proxyPort)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a slice of iptables args for a from-host public-port rule.
|
||||||
|
// See iptablesHostPortalArgs
|
||||||
|
// TODO: Should we just reuse iptablesHostPortalArgs?
|
||||||
|
func (proxier *Proxier) iptablesHostNodePortArgs(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service ServicePortName) []string {
|
||||||
|
args := iptablesCommonPortalArgs(nil, nodePort, protocol, service)
|
||||||
|
|
||||||
|
if proxyIP.Equal(zeroIPv4) || proxyIP.Equal(zeroIPv6) {
|
||||||
|
proxyIP = proxier.hostIP
|
||||||
|
}
|
||||||
|
// TODO: Can we DNAT with IPv6?
|
||||||
|
args = append(args, "-j", "DNAT", "--to-destination", net.JoinHostPort(proxyIP.String(), strconv.Itoa(proxyPort)))
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
func isTooManyFDsError(err error) bool {
|
func isTooManyFDsError(err error) bool {
|
||||||
return strings.Contains(err.Error(), "too many open files")
|
return strings.Contains(err.Error(), "too many open files")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func (fake *fakeIptables) FlushChain(table iptables.Table, chain iptables.Chain)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fake *fakeIptables) EnsureRule(table iptables.Table, chain iptables.Chain, args ...string) (bool, error) {
|
func (fake *fakeIptables) EnsureRule(position iptables.RulePosition, table iptables.Table, chain iptables.Chain, args ...string) (bool, error) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,7 +724,7 @@ func TestProxyUpdatePublicIPs(t *testing.T) {
|
|||||||
Protocol: "TCP",
|
Protocol: "TCP",
|
||||||
}},
|
}},
|
||||||
PortalIP: svcInfo.portalIP.String(),
|
PortalIP: svcInfo.portalIP.String(),
|
||||||
PublicIPs: []string{"4.3.2.1"},
|
DeprecatedPublicIPs: []string{"4.3.2.1"},
|
||||||
},
|
},
|
||||||
}})
|
}})
|
||||||
// Wait for the socket to actually get free.
|
// Wait for the socket to actually get free.
|
||||||
@@ -810,3 +810,5 @@ func TestProxyUpdatePortal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Test UDP timeouts.
|
// TODO: Test UDP timeouts.
|
||||||
|
|
||||||
|
// TODO(justinsb): Add test for nodePort conflict detection, once we have nodePort wired in
|
||||||
|
|||||||
@@ -601,6 +601,7 @@ func TestEtcdUpdateService(t *testing.T) {
|
|||||||
"baz": "bar",
|
"baz": "bar",
|
||||||
},
|
},
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
_, err := registry.UpdateService(ctx, &testService)
|
_, err := registry.UpdateService(ctx, &testService)
|
||||||
|
|||||||
168
pkg/registry/service/allocator/bitmap.go
Normal file
168
pkg/registry/service/allocator/bitmap.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 allocator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
"math/rand"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AllocationBitmap is a contiguous block of resources that can be allocated atomically.
|
||||||
|
//
|
||||||
|
// Each resource has an offset. The internal structure is a bitmap, with a bit for each offset.
|
||||||
|
//
|
||||||
|
// If a resource is taken, the bit at that offset is set to one.
|
||||||
|
// r.count is always equal to the number of set bits and can be recalculated at any time
|
||||||
|
// by counting the set bits in r.allocated.
|
||||||
|
//
|
||||||
|
// TODO: use RLE and compact the allocator to minimize space.
|
||||||
|
type AllocationBitmap struct {
|
||||||
|
// strategy is the strategy for choosing the next available item out of the range
|
||||||
|
strategy allocateStrategy
|
||||||
|
// max is the maximum size of the usable items in the range
|
||||||
|
max int
|
||||||
|
// rangeSpec is the range specifier, matching RangeAllocation.Range
|
||||||
|
rangeSpec string
|
||||||
|
|
||||||
|
// lock guards the following members
|
||||||
|
lock sync.Mutex
|
||||||
|
// count is the number of currently allocated elements in the range
|
||||||
|
count int
|
||||||
|
// allocated is a bit array of the allocated items in the range
|
||||||
|
allocated *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocationBitmap implements Interface and Snapshottable
|
||||||
|
var _ Interface = &AllocationBitmap{}
|
||||||
|
var _ Snapshottable = &AllocationBitmap{}
|
||||||
|
|
||||||
|
// allocateStrategy is a search strategy in the allocation map for a valid item.
|
||||||
|
type allocateStrategy func(allocated *big.Int, max, count int) (int, bool)
|
||||||
|
|
||||||
|
func NewAllocationMap(max int, rangeSpec string) *AllocationBitmap {
|
||||||
|
a := AllocationBitmap{
|
||||||
|
strategy: randomScanStrategy,
|
||||||
|
allocated: big.NewInt(0),
|
||||||
|
count: 0,
|
||||||
|
max: max,
|
||||||
|
rangeSpec: rangeSpec,
|
||||||
|
}
|
||||||
|
return &a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate attempts to reserve the provided item.
|
||||||
|
// Returns true if it was allocated, false if it was already in use
|
||||||
|
func (r *AllocationBitmap) Allocate(offset int) (bool, error) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
if r.allocated.Bit(offset) == 1 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
r.allocated = r.allocated.SetBit(r.allocated, offset, 1)
|
||||||
|
r.count++
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocateNext reserves one of the items from the pool.
|
||||||
|
// (0, false, nil) may be returned if there are no items left.
|
||||||
|
func (r *AllocationBitmap) AllocateNext() (int, bool, error) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
next, ok := r.strategy(r.allocated, r.max, r.count)
|
||||||
|
if !ok {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
r.count++
|
||||||
|
r.allocated = r.allocated.SetBit(r.allocated, next, 1)
|
||||||
|
return next, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases the item back to the pool. Releasing an
|
||||||
|
// unallocated item or an item out of the range is a no-op and
|
||||||
|
// returns no error.
|
||||||
|
func (r *AllocationBitmap) Release(offset int) error {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
if r.allocated.Bit(offset) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.allocated = r.allocated.SetBit(r.allocated, offset, 0)
|
||||||
|
r.count--
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has returns true if the provided item is already allocated and a call
|
||||||
|
// to Allocate(offset) would fail.
|
||||||
|
func (r *AllocationBitmap) Has(offset int) bool {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
return r.allocated.Bit(offset) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free returns the count of items left in the range.
|
||||||
|
func (r *AllocationBitmap) Free() int {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
return r.max - r.count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot saves the current state of the pool.
|
||||||
|
func (r *AllocationBitmap) Snapshot() (string, []byte) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
return r.rangeSpec, r.allocated.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the pool to the previously captured state.
|
||||||
|
func (r *AllocationBitmap) Restore(rangeSpec string, data []byte) error {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
|
if r.rangeSpec != rangeSpec {
|
||||||
|
return errors.New("the provided range does not match the current range")
|
||||||
|
}
|
||||||
|
|
||||||
|
r.allocated = big.NewInt(0).SetBytes(data)
|
||||||
|
r.count = countBits(r.allocated)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomScanStrategy chooses a random address from the provided big.Int, and then
|
||||||
|
// scans forward looking for the next available address (it will wrap the range if
|
||||||
|
// necessary).
|
||||||
|
func randomScanStrategy(allocated *big.Int, max, count int) (int, bool) {
|
||||||
|
if count >= max {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
offset := rand.Intn(max)
|
||||||
|
for i := 0; i < max; i++ {
|
||||||
|
at := (offset + i) % max
|
||||||
|
if allocated.Bit(at) == 0 {
|
||||||
|
return at, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
239
pkg/registry/service/allocator/etcd/etcd.go
Normal file
239
pkg/registry/service/allocator/etcd/etcd.go
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
k8serr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
||||||
|
etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errorUnableToAllocate = errors.New("unable to allocate")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Etcd exposes a service.Allocator that is backed by etcd.
|
||||||
|
// TODO: allow multiple allocations to be tried at once
|
||||||
|
// TODO: subdivide the keyspace to reduce conflicts
|
||||||
|
// TODO: investigate issuing a CAS without reading first
|
||||||
|
type Etcd struct {
|
||||||
|
lock sync.Mutex
|
||||||
|
|
||||||
|
alloc allocator.Snapshottable
|
||||||
|
helper tools.EtcdHelper
|
||||||
|
last string
|
||||||
|
|
||||||
|
baseKey string
|
||||||
|
kind string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Etcd implements allocator.Interface and service.RangeRegistry
|
||||||
|
var _ allocator.Interface = &Etcd{}
|
||||||
|
var _ service.RangeRegistry = &Etcd{}
|
||||||
|
|
||||||
|
// NewEtcd returns an allocator that is backed by Etcd and can manage
|
||||||
|
// persisting the snapshot state of allocation after each allocation is made.
|
||||||
|
func NewEtcd(alloc allocator.Snapshottable, baseKey string, kind string, helper tools.EtcdHelper) *Etcd {
|
||||||
|
return &Etcd{
|
||||||
|
alloc: alloc,
|
||||||
|
helper: helper,
|
||||||
|
baseKey: baseKey,
|
||||||
|
kind: kind,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate attempts to allocate the item locally and then in etcd.
|
||||||
|
func (e *Etcd) Allocate(offset int) (bool, error) {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
ok, err := e.alloc.Allocate(offset)
|
||||||
|
if !ok || err != nil {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.tryUpdate(func() error {
|
||||||
|
ok, err := e.alloc.Allocate(offset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return errorUnableToAllocate
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == errorUnableToAllocate {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocateNext attempts to allocate the next item locally and then in etcd.
|
||||||
|
func (e *Etcd) AllocateNext() (int, bool, error) {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
offset, ok, err := e.alloc.AllocateNext()
|
||||||
|
if !ok || err != nil {
|
||||||
|
return offset, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.tryUpdate(func() error {
|
||||||
|
ok, err := e.alloc.Allocate(offset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
// update the offset here
|
||||||
|
offset, ok, err = e.alloc.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return errorUnableToAllocate
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return offset, ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release attempts to release the provided item locally and then in etcd.
|
||||||
|
func (e *Etcd) Release(item int) error {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
if err := e.alloc.Release(item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.tryUpdate(func() error {
|
||||||
|
return e.alloc.Release(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryUpdate performs a read-update to persist the latest snapshot state of allocation.
|
||||||
|
func (e *Etcd) tryUpdate(fn func() error) error {
|
||||||
|
err := e.helper.GuaranteedUpdate(e.baseKey, &api.RangeAllocation{}, true,
|
||||||
|
func(input runtime.Object) (output runtime.Object, ttl uint64, err error) {
|
||||||
|
existing := input.(*api.RangeAllocation)
|
||||||
|
if len(existing.ResourceVersion) == 0 {
|
||||||
|
return nil, 0, fmt.Errorf("cannot allocate resources of type %s at this time", e.kind)
|
||||||
|
}
|
||||||
|
if existing.ResourceVersion != e.last {
|
||||||
|
if err := e.alloc.Restore(existing.Range, existing.Data); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if err := fn(); err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.last = existing.ResourceVersion
|
||||||
|
rangeSpec, data := e.alloc.Snapshot()
|
||||||
|
existing.Range = rangeSpec
|
||||||
|
existing.Data = data
|
||||||
|
return existing, 0, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return etcderr.InterpretUpdateError(err, e.kind, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh reloads the RangeAllocation from etcd.
|
||||||
|
func (e *Etcd) Refresh() (*api.RangeAllocation, error) {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
existing := &api.RangeAllocation{}
|
||||||
|
if err := e.helper.ExtractObj(e.baseKey, existing, false); err != nil {
|
||||||
|
if tools.IsEtcdNotFound(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, etcderr.InterpretGetError(err, e.kind, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns an api.RangeAllocation that represents the current state in
|
||||||
|
// etcd. If the key does not exist, the object will have an empty ResourceVersion.
|
||||||
|
func (e *Etcd) Get() (*api.RangeAllocation, error) {
|
||||||
|
existing := &api.RangeAllocation{}
|
||||||
|
if err := e.helper.ExtractObj(e.baseKey, existing, true); err != nil {
|
||||||
|
return nil, etcderr.InterpretGetError(err, e.kind, "")
|
||||||
|
}
|
||||||
|
return existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdate attempts to update the current etcd state with the provided
|
||||||
|
// allocation.
|
||||||
|
func (e *Etcd) CreateOrUpdate(snapshot *api.RangeAllocation) error {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
last := ""
|
||||||
|
err := e.helper.GuaranteedUpdate(e.baseKey, &api.RangeAllocation{}, true,
|
||||||
|
func(input runtime.Object) (output runtime.Object, ttl uint64, err error) {
|
||||||
|
existing := input.(*api.RangeAllocation)
|
||||||
|
switch {
|
||||||
|
case len(snapshot.ResourceVersion) != 0 && len(existing.ResourceVersion) != 0:
|
||||||
|
if snapshot.ResourceVersion != existing.ResourceVersion {
|
||||||
|
return nil, 0, k8serr.NewConflict(e.kind, "", fmt.Errorf("the provided resource version does not match"))
|
||||||
|
}
|
||||||
|
case len(existing.ResourceVersion) != 0:
|
||||||
|
return nil, 0, k8serr.NewConflict(e.kind, "", fmt.Errorf("another caller has already initialized the resource"))
|
||||||
|
}
|
||||||
|
last = snapshot.ResourceVersion
|
||||||
|
return snapshot, 0, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return etcderr.InterpretUpdateError(err, e.kind, "")
|
||||||
|
}
|
||||||
|
err = e.alloc.Restore(snapshot.Range, snapshot.Data)
|
||||||
|
if err == nil {
|
||||||
|
e.last = last
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements allocator.Interface::Has
|
||||||
|
func (e *Etcd) Has(item int) bool {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
return e.alloc.Has(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements allocator.Interface::Free
|
||||||
|
func (e *Etcd) Free() int {
|
||||||
|
e.lock.Lock()
|
||||||
|
defer e.lock.Unlock()
|
||||||
|
|
||||||
|
return e.alloc.Free()
|
||||||
|
}
|
||||||
125
pkg/registry/service/allocator/etcd/etcd_test.go
Normal file
125
pkg/registry/service/allocator/etcd/etcd_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 etcd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/go-etcd/etcd"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) {
|
||||||
|
fakeEtcdClient := tools.NewFakeEtcdClient(t)
|
||||||
|
fakeEtcdClient.TestIndex = true
|
||||||
|
helper := tools.NewEtcdHelper(fakeEtcdClient, testapi.Codec(), etcdtest.PathPrefix())
|
||||||
|
return fakeEtcdClient, helper
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStorage(t *testing.T) (*Etcd, allocator.Interface, *tools.FakeEtcdClient) {
|
||||||
|
fakeEtcdClient, h := newHelper(t)
|
||||||
|
|
||||||
|
mem := allocator.NewAllocationMap(100, "rangeSpecValue")
|
||||||
|
etcd := NewEtcd(mem, "/ranges/serviceips", "serviceipallocation", h)
|
||||||
|
|
||||||
|
return etcd, mem, fakeEtcdClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func key() string {
|
||||||
|
s := "/ranges/serviceips"
|
||||||
|
return etcdtest.AddPrefix(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmpty(t *testing.T) {
|
||||||
|
storage, _, ecli := newStorage(t)
|
||||||
|
ecli.ExpectNotFoundGet(key())
|
||||||
|
if _, err := storage.Allocate(1); fmt.Sprintf("%v", err) != "cannot allocate resources of type serviceipallocation at this time" {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialObject(ecli *tools.FakeEtcdClient) {
|
||||||
|
ecli.Data[key()] = tools.EtcdResponseWithError{
|
||||||
|
R: &etcd.Response{
|
||||||
|
Node: &etcd.Node{
|
||||||
|
CreatedIndex: 1,
|
||||||
|
ModifiedIndex: 2,
|
||||||
|
Value: runtime.EncodeOrDie(testapi.Codec(), &api.RangeAllocation{
|
||||||
|
Range: "rangeSpecValue",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
E: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
storage, backing, ecli := newStorage(t)
|
||||||
|
initialObject(ecli)
|
||||||
|
|
||||||
|
if _, err := storage.Allocate(2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ok, err := backing.Allocate(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
t.Fatal("Expected backing allocation to fail")
|
||||||
|
}
|
||||||
|
if ok, err := storage.Allocate(2); ok || err != nil {
|
||||||
|
t.Fatal("Expected allocation to fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
obj := ecli.Data[key()]
|
||||||
|
if obj.R == nil || obj.R.Node == nil {
|
||||||
|
t.Fatalf("%s is empty: %#v", key(), obj)
|
||||||
|
}
|
||||||
|
t.Logf("data: %#v", obj.R.Node)
|
||||||
|
|
||||||
|
other := allocator.NewAllocationMap(100, "rangeSpecValue")
|
||||||
|
|
||||||
|
allocation := &api.RangeAllocation{}
|
||||||
|
if err := storage.helper.ExtractObj(key(), allocation, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if allocation.ResourceVersion != "1" {
|
||||||
|
t.Fatalf("%#v", allocation)
|
||||||
|
}
|
||||||
|
if allocation.Range != "rangeSpecValue" {
|
||||||
|
t.Errorf("unexpected stored Range: %s", allocation.Range)
|
||||||
|
}
|
||||||
|
if err := other.Restore("rangeSpecValue", allocation.Data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !other.Has(2) {
|
||||||
|
t.Fatalf("could not restore allocated IP: %#v", other)
|
||||||
|
}
|
||||||
|
|
||||||
|
other = allocator.NewAllocationMap(100, "rangeSpecValue")
|
||||||
|
otherStorage := NewEtcd(other, "/ranges/serviceips", "serviceipallocation", storage.helper)
|
||||||
|
if ok, err := otherStorage.Allocate(2); ok || err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
41
pkg/registry/service/allocator/interfaces.go
Normal file
41
pkg/registry/service/allocator/interfaces.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 The Kubernetes Authors 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 allocator
|
||||||
|
|
||||||
|
// Interface manages the allocation of items out of a range. Interface
|
||||||
|
// should be threadsafe.
|
||||||
|
type Interface interface {
|
||||||
|
Allocate(int) (bool, error)
|
||||||
|
AllocateNext() (int, bool, error)
|
||||||
|
Release(int) error
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
Has(int) bool
|
||||||
|
|
||||||
|
// For testing
|
||||||
|
Free() int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshottable is an Interface that can be snapshotted and restored. Snapshottable
|
||||||
|
// should be threadsafe.
|
||||||
|
type Snapshottable interface {
|
||||||
|
Interface
|
||||||
|
Snapshot() (string, []byte)
|
||||||
|
Restore(string, []byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type AllocatorFactory func(max int, rangeSpec string) Interface
|
||||||
64
pkg/registry/service/allocator/utils.go
Normal file
64
pkg/registry/service/allocator/utils.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 allocator
|
||||||
|
|
||||||
|
import "math/big"
|
||||||
|
|
||||||
|
// countBits returns the number of set bits in n
|
||||||
|
func countBits(n *big.Int) int {
|
||||||
|
var count int = 0
|
||||||
|
for _, b := range n.Bytes() {
|
||||||
|
count += int(bitCounts[b])
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitCounts is all of the bits counted for each number between 0-255
|
||||||
|
var bitCounts = []int8{
|
||||||
|
0, 1, 1, 2, 1, 2, 2, 3,
|
||||||
|
1, 2, 2, 3, 2, 3, 3, 4,
|
||||||
|
1, 2, 2, 3, 2, 3, 3, 4,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
1, 2, 2, 3, 2, 3, 3, 4,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
1, 2, 2, 3, 2, 3, 3, 4,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
4, 5, 5, 6, 5, 6, 6, 7,
|
||||||
|
1, 2, 2, 3, 2, 3, 3, 4,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
4, 5, 5, 6, 5, 6, 6, 7,
|
||||||
|
2, 3, 3, 4, 3, 4, 4, 5,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
4, 5, 5, 6, 5, 6, 6, 7,
|
||||||
|
3, 4, 4, 5, 4, 5, 5, 6,
|
||||||
|
4, 5, 5, 6, 5, 6, 6, 7,
|
||||||
|
4, 5, 5, 6, 5, 6, 6, 7,
|
||||||
|
5, 6, 6, 7, 6, 7, 7, 8,
|
||||||
|
}
|
||||||
33
pkg/registry/service/allocator/utils_test.go
Normal file
33
pkg/registry/service/allocator/utils_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 allocator
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestBitCount(t *testing.T) {
|
||||||
|
for i, c := range bitCounts {
|
||||||
|
actual := 0
|
||||||
|
for j := 0; j < 8; j++ {
|
||||||
|
if ((1 << uint(j)) & i) != 0 {
|
||||||
|
actual++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if actual != int(c) {
|
||||||
|
t.Errorf("%d should have %d bits but recorded as %d", i, actual, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,10 @@ package ipallocator
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
|
||||||
"math/big"
|
"math/big"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Interface manages the allocation of IP addresses out of a range. Interface
|
// Interface manages the allocation of IP addresses out of a range. Interface
|
||||||
@@ -33,20 +33,11 @@ type Interface interface {
|
|||||||
Release(net.IP) error
|
Release(net.IP) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshottable is an Interface that can be snapshotted and restored. Snapshottable
|
|
||||||
// should be threadsafe.
|
|
||||||
type Snapshottable interface {
|
|
||||||
Interface
|
|
||||||
Snapshot() (*net.IPNet, []byte)
|
|
||||||
Restore(*net.IPNet, []byte) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrFull = errors.New("range is full")
|
ErrFull = errors.New("range is full")
|
||||||
ErrNotInRange = errors.New("provided IP is not in the valid range")
|
ErrNotInRange = errors.New("provided IP is not in the valid range")
|
||||||
ErrAllocated = errors.New("provided IP is already allocated")
|
ErrAllocated = errors.New("provided IP is already allocated")
|
||||||
ErrMismatchedNetwork = errors.New("the provided network does not match the current range")
|
ErrMismatchedNetwork = errors.New("the provided network does not match the current range")
|
||||||
ErrAllocationDisabled = errors.New("IP addresses cannot be allocated at this time")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Range is a contiguous block of IPs that can be allocated atomically.
|
// Range is a contiguous block of IPs that can be allocated atomically.
|
||||||
@@ -64,52 +55,39 @@ var (
|
|||||||
// | |
|
// | |
|
||||||
// r.base r.base + r.max
|
// r.base r.base + r.max
|
||||||
// | |
|
// | |
|
||||||
// first bit of r.allocated last bit of r.allocated
|
// offset #0 of r.allocated last offset of r.allocated
|
||||||
//
|
|
||||||
// If an address is taken, the bit at offset:
|
|
||||||
//
|
|
||||||
// bit offset := IP - r.base
|
|
||||||
//
|
|
||||||
// is set to one. r.count is always equal to the number of set bits and
|
|
||||||
// can be recalculated at any time by counting the set bits in r.allocated.
|
|
||||||
//
|
|
||||||
// TODO: use RLE and compact the allocator to minimize space.
|
|
||||||
type Range struct {
|
type Range struct {
|
||||||
net *net.IPNet
|
net *net.IPNet
|
||||||
// base is a cached version of the start IP in the CIDR range as a *big.Int
|
// base is a cached version of the start IP in the CIDR range as a *big.Int
|
||||||
base *big.Int
|
base *big.Int
|
||||||
// strategy is the strategy for choosing the next available IP out of the range
|
|
||||||
strategy allocateStrategy
|
|
||||||
// max is the maximum size of the usable addresses in the range
|
// max is the maximum size of the usable addresses in the range
|
||||||
max int
|
max int
|
||||||
|
|
||||||
// lock guards the following members
|
alloc allocator.Interface
|
||||||
lock sync.Mutex
|
|
||||||
// count is the number of currently allocated elements in the range
|
|
||||||
count int
|
|
||||||
// allocated is a bit array of the allocated ips in the range
|
|
||||||
allocated *big.Int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// allocateStrategy is a search strategy in the allocation map for a valid IP.
|
// NewAllocatorCIDRRange creates a Range over a net.IPNet, calling allocatorFactory to construct the backing store.
|
||||||
type allocateStrategy func(allocated *big.Int, max, count int) (int, error)
|
func NewAllocatorCIDRRange(cidr *net.IPNet, allocatorFactory allocator.AllocatorFactory) *Range {
|
||||||
|
|
||||||
// NewCIDRRange creates a Range over a net.IPNet.
|
|
||||||
func NewCIDRRange(cidr *net.IPNet) *Range {
|
|
||||||
max := RangeSize(cidr)
|
max := RangeSize(cidr)
|
||||||
base := bigForIP(cidr.IP)
|
base := bigForIP(cidr.IP)
|
||||||
|
rangeSpec := cidr.String()
|
||||||
|
|
||||||
r := Range{
|
r := Range{
|
||||||
net: cidr,
|
net: cidr,
|
||||||
strategy: randomScanStrategy,
|
|
||||||
base: base.Add(base, big.NewInt(1)), // don't use the network base
|
base: base.Add(base, big.NewInt(1)), // don't use the network base
|
||||||
max: maximum(0, int(max-2)), // don't use the network broadcast
|
max: maximum(0, int(max-2)), // don't use the network broadcast,
|
||||||
|
|
||||||
allocated: big.NewInt(0),
|
|
||||||
count: 0,
|
|
||||||
}
|
}
|
||||||
|
r.alloc = allocatorFactory(r.max, rangeSpec)
|
||||||
return &r
|
return &r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper that wraps NewAllocatorCIDRRange, for creating a range backed by an in-memory store.
|
||||||
|
func NewCIDRRange(cidr *net.IPNet) *Range {
|
||||||
|
return NewAllocatorCIDRRange(cidr, func(max int, rangeSpec string) allocator.Interface {
|
||||||
|
return allocator.NewAllocationMap(max, rangeSpec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func maximum(a, b int) int {
|
func maximum(a, b int) int {
|
||||||
if a > b {
|
if a > b {
|
||||||
return a
|
return a
|
||||||
@@ -119,9 +97,7 @@ func maximum(a, b int) int {
|
|||||||
|
|
||||||
// Free returns the count of IP addresses left in the range.
|
// Free returns the count of IP addresses left in the range.
|
||||||
func (r *Range) Free() int {
|
func (r *Range) Free() int {
|
||||||
r.lock.Lock()
|
return r.alloc.Free()
|
||||||
defer r.lock.Unlock()
|
|
||||||
return r.max - r.count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate attempts to reserve the provided IP. ErrNotInRange or
|
// Allocate attempts to reserve the provided IP. ErrNotInRange or
|
||||||
@@ -134,30 +110,27 @@ func (r *Range) Allocate(ip net.IP) error {
|
|||||||
return ErrNotInRange
|
return ErrNotInRange
|
||||||
}
|
}
|
||||||
|
|
||||||
r.lock.Lock()
|
allocated, err := r.alloc.Allocate(offset)
|
||||||
defer r.lock.Unlock()
|
if err != nil {
|
||||||
|
return err
|
||||||
if r.allocated.Bit(offset) == 1 {
|
}
|
||||||
|
if !allocated {
|
||||||
return ErrAllocated
|
return ErrAllocated
|
||||||
}
|
}
|
||||||
r.allocated = r.allocated.SetBit(r.allocated, offset, 1)
|
|
||||||
r.count++
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllocateNext reserves one of the IPs from the pool. ErrFull may
|
// AllocateNext reserves one of the IPs from the pool. ErrFull may
|
||||||
// be returned if there are no addresses left.
|
// be returned if there are no addresses left.
|
||||||
func (r *Range) AllocateNext() (net.IP, error) {
|
func (r *Range) AllocateNext() (net.IP, error) {
|
||||||
r.lock.Lock()
|
offset, ok, err := r.alloc.AllocateNext()
|
||||||
defer r.lock.Unlock()
|
|
||||||
|
|
||||||
next, err := r.strategy(r.allocated, r.max, r.count)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
r.count++
|
if !ok {
|
||||||
r.allocated = r.allocated.SetBit(r.allocated, next, 1)
|
return nil, ErrFull
|
||||||
return addIPOffset(r.base, next), nil
|
}
|
||||||
|
return addIPOffset(r.base, offset), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release releases the IP back to the pool. Releasing an
|
// Release releases the IP back to the pool. Releasing an
|
||||||
@@ -169,16 +142,7 @@ func (r *Range) Release(ip net.IP) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
r.lock.Lock()
|
return r.alloc.Release(offset)
|
||||||
defer r.lock.Unlock()
|
|
||||||
|
|
||||||
if r.allocated.Bit(offset) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r.allocated = r.allocated.SetBit(r.allocated, offset, 0)
|
|
||||||
r.count--
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has returns true if the provided IP is already allocated and a call
|
// Has returns true if the provided IP is already allocated and a call
|
||||||
@@ -189,31 +153,32 @@ func (r *Range) Has(ip net.IP) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
r.lock.Lock()
|
return r.alloc.Has(offset)
|
||||||
defer r.lock.Unlock()
|
|
||||||
|
|
||||||
return r.allocated.Bit(offset) == 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot saves the current state of the pool.
|
// Snapshot saves the current state of the pool.
|
||||||
func (r *Range) Snapshot() (*net.IPNet, []byte) {
|
func (r *Range) Snapshot(dst *api.RangeAllocation) error {
|
||||||
r.lock.Lock()
|
snapshottable, ok := r.alloc.(allocator.Snapshottable)
|
||||||
defer r.lock.Unlock()
|
if !ok {
|
||||||
|
return fmt.Errorf("not a snapshottable allocator")
|
||||||
return r.net, r.allocated.Bytes()
|
}
|
||||||
|
rangeString, data := snapshottable.Snapshot()
|
||||||
|
dst.Range = rangeString
|
||||||
|
dst.Data = data
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore restores the pool to the previously captured state. ErrMismatchedNetwork
|
// Restore restores the pool to the previously captured state. ErrMismatchedNetwork
|
||||||
// is returned if the provided IPNet range doesn't exactly match the previous range.
|
// is returned if the provided IPNet range doesn't exactly match the previous range.
|
||||||
func (r *Range) Restore(net *net.IPNet, data []byte) error {
|
func (r *Range) Restore(net *net.IPNet, data []byte) error {
|
||||||
r.lock.Lock()
|
|
||||||
defer r.lock.Unlock()
|
|
||||||
|
|
||||||
if !net.IP.Equal(r.net.IP) || net.Mask.String() != r.net.Mask.String() {
|
if !net.IP.Equal(r.net.IP) || net.Mask.String() != r.net.Mask.String() {
|
||||||
return ErrMismatchedNetwork
|
return ErrMismatchedNetwork
|
||||||
}
|
}
|
||||||
r.allocated = big.NewInt(0).SetBytes(data)
|
snapshottable, ok := r.alloc.(allocator.Snapshottable)
|
||||||
r.count = countBits(r.allocated)
|
if !ok {
|
||||||
|
return fmt.Errorf("not a snapshottable allocator")
|
||||||
|
}
|
||||||
|
snapshottable.Restore(net.String(), data)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,68 +217,6 @@ func calculateIPOffset(base *big.Int, ip net.IP) int {
|
|||||||
return int(big.NewInt(0).Sub(bigForIP(ip), base).Int64())
|
return int(big.NewInt(0).Sub(bigForIP(ip), base).Int64())
|
||||||
}
|
}
|
||||||
|
|
||||||
// randomScanStrategy chooses a random address from the provided big.Int, and then
|
|
||||||
// scans forward looking for the next available address (it will wrap the range if
|
|
||||||
// necessary).
|
|
||||||
func randomScanStrategy(allocated *big.Int, max, count int) (int, error) {
|
|
||||||
if count >= max {
|
|
||||||
return 0, ErrFull
|
|
||||||
}
|
|
||||||
offset := rand.Intn(max)
|
|
||||||
for i := 0; i < max; i++ {
|
|
||||||
at := (offset + i) % max
|
|
||||||
if allocated.Bit(at) == 0 {
|
|
||||||
return at, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, ErrFull
|
|
||||||
}
|
|
||||||
|
|
||||||
// countBits returns the number of set bits in n
|
|
||||||
func countBits(n *big.Int) int {
|
|
||||||
var count int = 0
|
|
||||||
for _, b := range n.Bytes() {
|
|
||||||
count += int(bitCounts[b])
|
|
||||||
}
|
|
||||||
return count
|
|
||||||
}
|
|
||||||
|
|
||||||
// bitCounts is all of the bits counted for each number between 0-255
|
|
||||||
var bitCounts = []int8{
|
|
||||||
0, 1, 1, 2, 1, 2, 2, 3,
|
|
||||||
1, 2, 2, 3, 2, 3, 3, 4,
|
|
||||||
1, 2, 2, 3, 2, 3, 3, 4,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
1, 2, 2, 3, 2, 3, 3, 4,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
1, 2, 2, 3, 2, 3, 3, 4,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
4, 5, 5, 6, 5, 6, 6, 7,
|
|
||||||
1, 2, 2, 3, 2, 3, 3, 4,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
4, 5, 5, 6, 5, 6, 6, 7,
|
|
||||||
2, 3, 3, 4, 3, 4, 4, 5,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
4, 5, 5, 6, 5, 6, 6, 7,
|
|
||||||
3, 4, 4, 5, 4, 5, 5, 6,
|
|
||||||
4, 5, 5, 6, 5, 6, 6, 7,
|
|
||||||
4, 5, 5, 6, 5, 6, 6, 7,
|
|
||||||
5, 6, 6, 7, 6, 7, 7, 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
// RangeSize returns the size of a range in valid addresses.
|
// RangeSize returns the size of a range in valid addresses.
|
||||||
func RangeSize(subnet *net.IPNet) int64 {
|
func RangeSize(subnet *net.IPNet) int64 {
|
||||||
ones, bits := subnet.Mask.Size()
|
ones, bits := subnet.Mask.Size()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -142,27 +143,13 @@ func TestAllocateSmall(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.count != 2 && r.Free() != 0 && r.max != 2 {
|
if r.Free() != 0 && r.max != 2 {
|
||||||
t.Fatalf("unexpected range: %v", r)
|
t.Fatalf("unexpected range: %v", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("allocated: %v", found)
|
t.Logf("allocated: %v", found)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBitCount(t *testing.T) {
|
|
||||||
for i, c := range bitCounts {
|
|
||||||
actual := 0
|
|
||||||
for j := 0; j < 8; j++ {
|
|
||||||
if ((1 << uint(j)) & i) != 0 {
|
|
||||||
actual++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if actual != int(c) {
|
|
||||||
t.Errorf("%d should have %d bits but recorded as %d", i, actual, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRangeSize(t *testing.T) {
|
func TestRangeSize(t *testing.T) {
|
||||||
testCases := map[string]int64{
|
testCases := map[string]int64{
|
||||||
"192.168.1.0/24": 256,
|
"192.168.1.0/24": 256,
|
||||||
@@ -195,7 +182,17 @@ func TestSnapshot(t *testing.T) {
|
|||||||
ip = append(ip, n)
|
ip = append(ip, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
network, data := r.Snapshot()
|
var dst api.RangeAllocation
|
||||||
|
err = r.Snapshot(&dst)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, network, err := net.ParseCIDR(dst.Range)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
if !network.IP.Equal(cidr.IP) || network.Mask.String() != cidr.Mask.String() {
|
if !network.IP.Equal(cidr.IP) || network.Mask.String() != cidr.Mask.String() {
|
||||||
t.Fatalf("mismatched networks: %s : %s", network, cidr)
|
t.Fatalf("mismatched networks: %s : %s", network, cidr)
|
||||||
}
|
}
|
||||||
@@ -205,11 +202,11 @@ func TestSnapshot(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
other := NewCIDRRange(otherCidr)
|
other := NewCIDRRange(otherCidr)
|
||||||
if err := r.Restore(otherCidr, data); err != ErrMismatchedNetwork {
|
if err := r.Restore(otherCidr, dst.Data); err != ErrMismatchedNetwork {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
other = NewCIDRRange(network)
|
other = NewCIDRRange(network)
|
||||||
if err := other.Restore(network, data); err != nil {
|
if err := other.Restore(network, dst.Data); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ type Repair struct {
|
|||||||
interval time.Duration
|
interval time.Duration
|
||||||
registry service.Registry
|
registry service.Registry
|
||||||
network *net.IPNet
|
network *net.IPNet
|
||||||
alloc service.IPRegistry
|
alloc service.RangeRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRepair creates a controller that periodically ensures that all portalIPs are uniquely allocated across the cluster
|
// NewRepair creates a controller that periodically ensures that all portalIPs are uniquely allocated across the cluster
|
||||||
// and generates informational warnings for a cluster that is not in sync.
|
// and generates informational warnings for a cluster that is not in sync.
|
||||||
func NewRepair(interval time.Duration, registry service.Registry, network *net.IPNet, alloc service.IPRegistry) *Repair {
|
func NewRepair(interval time.Duration, registry service.Registry, network *net.IPNet, alloc service.RangeRegistry) *Repair {
|
||||||
return &Repair{
|
return &Repair{
|
||||||
interval: interval,
|
interval: interval,
|
||||||
registry: registry,
|
registry: registry,
|
||||||
@@ -71,6 +71,13 @@ func (c *Repair) RunUntil(ch chan struct{}) {
|
|||||||
|
|
||||||
// RunOnce verifies the state of the portal IP allocations and returns an error if an unrecoverable problem occurs.
|
// RunOnce verifies the state of the portal IP allocations and returns an error if an unrecoverable problem occurs.
|
||||||
func (c *Repair) RunOnce() error {
|
func (c *Repair) RunOnce() error {
|
||||||
|
// TODO: (per smarterclayton) if Get() or ListServices() is a weak consistency read,
|
||||||
|
// or if they are executed against different leaders,
|
||||||
|
// the ordering guarantee required to ensure no IP is allocated twice is violated.
|
||||||
|
// ListServices must return a ResourceVersion higher than the etcd index Get triggers,
|
||||||
|
// and the release code must not release services that have had IPs allocated but not yet been created
|
||||||
|
// See #8295
|
||||||
|
|
||||||
latest, err := c.alloc.Get()
|
latest, err := c.alloc.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to refresh the service IP block: %v", err)
|
return fmt.Errorf("unable to refresh the service IP block: %v", err)
|
||||||
@@ -111,7 +118,10 @@ func (c *Repair) RunOnce() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
service.SnapshotRange(latest, r)
|
err = r.Snapshot(latest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to persist the updated service IP allocations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := c.alloc.CreateOrUpdate(latest); err != nil {
|
if err := c.alloc.CreateOrUpdate(latest); err != nil {
|
||||||
return fmt.Errorf("unable to persist the updated service IP allocations: %v", err)
|
return fmt.Errorf("unable to persist the updated service IP allocations: %v", err)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockIPRegistry struct {
|
type mockRangeRegistry struct {
|
||||||
getCalled bool
|
getCalled bool
|
||||||
item *api.RangeAllocation
|
item *api.RangeAllocation
|
||||||
err error
|
err error
|
||||||
@@ -37,12 +37,12 @@ type mockIPRegistry struct {
|
|||||||
updateErr error
|
updateErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mockIPRegistry) Get() (*api.RangeAllocation, error) {
|
func (r *mockRangeRegistry) Get() (*api.RangeAllocation, error) {
|
||||||
r.getCalled = true
|
r.getCalled = true
|
||||||
return r.item, r.err
|
return r.item, r.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *mockIPRegistry) CreateOrUpdate(alloc *api.RangeAllocation) error {
|
func (r *mockRangeRegistry) CreateOrUpdate(alloc *api.RangeAllocation) error {
|
||||||
r.updateCalled = true
|
r.updateCalled = true
|
||||||
r.updated = alloc
|
r.updated = alloc
|
||||||
return r.updateErr
|
return r.updateErr
|
||||||
@@ -51,7 +51,7 @@ func (r *mockIPRegistry) CreateOrUpdate(alloc *api.RangeAllocation) error {
|
|||||||
func TestRepair(t *testing.T) {
|
func TestRepair(t *testing.T) {
|
||||||
registry := registrytest.NewServiceRegistry()
|
registry := registrytest.NewServiceRegistry()
|
||||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
||||||
ipregistry := &mockIPRegistry{
|
ipregistry := &mockRangeRegistry{
|
||||||
item: &api.RangeAllocation{},
|
item: &api.RangeAllocation{},
|
||||||
}
|
}
|
||||||
r := NewRepair(0, registry, cidr, ipregistry)
|
r := NewRepair(0, registry, cidr, ipregistry)
|
||||||
@@ -63,7 +63,7 @@ func TestRepair(t *testing.T) {
|
|||||||
t.Errorf("unexpected ipregistry: %#v", ipregistry)
|
t.Errorf("unexpected ipregistry: %#v", ipregistry)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipregistry = &mockIPRegistry{
|
ipregistry = &mockRangeRegistry{
|
||||||
item: &api.RangeAllocation{},
|
item: &api.RangeAllocation{},
|
||||||
updateErr: fmt.Errorf("test error"),
|
updateErr: fmt.Errorf("test error"),
|
||||||
}
|
}
|
||||||
@@ -77,16 +77,21 @@ func TestRepairEmpty(t *testing.T) {
|
|||||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
||||||
previous := ipallocator.NewCIDRRange(cidr)
|
previous := ipallocator.NewCIDRRange(cidr)
|
||||||
previous.Allocate(net.ParseIP("192.168.1.10"))
|
previous.Allocate(net.ParseIP("192.168.1.10"))
|
||||||
network, data := previous.Snapshot()
|
|
||||||
|
var dst api.RangeAllocation
|
||||||
|
err := previous.Snapshot(&dst)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
registry := registrytest.NewServiceRegistry()
|
registry := registrytest.NewServiceRegistry()
|
||||||
ipregistry := &mockIPRegistry{
|
ipregistry := &mockRangeRegistry{
|
||||||
item: &api.RangeAllocation{
|
item: &api.RangeAllocation{
|
||||||
ObjectMeta: api.ObjectMeta{
|
ObjectMeta: api.ObjectMeta{
|
||||||
ResourceVersion: "1",
|
ResourceVersion: "1",
|
||||||
},
|
},
|
||||||
Range: network.String(),
|
Range: dst.Range,
|
||||||
Data: data,
|
Data: dst.Data,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r := NewRepair(0, registry, cidr, ipregistry)
|
r := NewRepair(0, registry, cidr, ipregistry)
|
||||||
@@ -105,7 +110,13 @@ func TestRepairEmpty(t *testing.T) {
|
|||||||
func TestRepairWithExisting(t *testing.T) {
|
func TestRepairWithExisting(t *testing.T) {
|
||||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
||||||
previous := ipallocator.NewCIDRRange(cidr)
|
previous := ipallocator.NewCIDRRange(cidr)
|
||||||
network, data := previous.Snapshot()
|
|
||||||
|
var dst api.RangeAllocation
|
||||||
|
err := previous.Snapshot(&dst)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
registry := registrytest.NewServiceRegistry()
|
registry := registrytest.NewServiceRegistry()
|
||||||
registry.List = api.ServiceList{
|
registry.List = api.ServiceList{
|
||||||
Items: []api.Service{
|
Items: []api.Service{
|
||||||
@@ -130,13 +141,13 @@ func TestRepairWithExisting(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
ipregistry := &mockIPRegistry{
|
ipregistry := &mockRangeRegistry{
|
||||||
item: &api.RangeAllocation{
|
item: &api.RangeAllocation{
|
||||||
ObjectMeta: api.ObjectMeta{
|
ObjectMeta: api.ObjectMeta{
|
||||||
ResourceVersion: "1",
|
ResourceVersion: "1",
|
||||||
},
|
},
|
||||||
Range: network.String(),
|
Range: dst.Range,
|
||||||
Data: data,
|
Data: dst.Data,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
r := NewRepair(0, registry, cidr, ipregistry)
|
r := NewRepair(0, registry, cidr, ipregistry)
|
||||||
|
|||||||
@@ -16,179 +16,4 @@ limitations under the License.
|
|||||||
|
|
||||||
package etcd
|
package etcd
|
||||||
|
|
||||||
import (
|
// Keep CI happy; it is unhappy if a directory only contains tests
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
|
|
||||||
etcderr "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors/etcd"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Etcd exposes a service.Allocator that is backed by etcd.
|
|
||||||
// TODO: allow multiple allocations to be tried at once
|
|
||||||
// TODO: subdivide the keyspace to reduce conflicts
|
|
||||||
// TODO: investigate issuing a CAS without reading first
|
|
||||||
type Etcd struct {
|
|
||||||
lock sync.Mutex
|
|
||||||
|
|
||||||
alloc ipallocator.Snapshottable
|
|
||||||
helper tools.EtcdHelper
|
|
||||||
last string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Etcd implements ipallocator.Interface and service.IPRegistry
|
|
||||||
var _ ipallocator.Interface = &Etcd{}
|
|
||||||
var _ service.IPRegistry = &Etcd{}
|
|
||||||
|
|
||||||
const baseKey = "/ranges/serviceips"
|
|
||||||
|
|
||||||
// NewEtcd returns a service PortalIP ipallocator that is backed by Etcd and can manage
|
|
||||||
// persisting the snapshot state of allocation after each allocation is made.
|
|
||||||
func NewEtcd(alloc ipallocator.Snapshottable, helper tools.EtcdHelper) *Etcd {
|
|
||||||
return &Etcd{
|
|
||||||
alloc: alloc,
|
|
||||||
helper: helper,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate attempts to allocate the IP locally and then in etcd.
|
|
||||||
func (e *Etcd) Allocate(ip net.IP) error {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
if err := e.alloc.Allocate(ip); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.tryUpdate(func() error {
|
|
||||||
return e.alloc.Allocate(ip)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// AllocateNext attempts to allocate the next IP locally and then in etcd.
|
|
||||||
func (e *Etcd) AllocateNext() (net.IP, error) {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
ip, err := e.alloc.AllocateNext()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = e.tryUpdate(func() error {
|
|
||||||
if err := e.alloc.Allocate(ip); err != nil {
|
|
||||||
if err != ipallocator.ErrAllocated {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// update the ip here
|
|
||||||
ip, err = e.alloc.AllocateNext()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return ip, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release attempts to release the provided IP locally and then in etcd.
|
|
||||||
func (e *Etcd) Release(ip net.IP) error {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
if err := e.alloc.Release(ip); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.tryUpdate(func() error {
|
|
||||||
return e.alloc.Release(ip)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryUpdate performs a read-update to persist the latest snapshot state of allocation.
|
|
||||||
func (e *Etcd) tryUpdate(fn func() error) error {
|
|
||||||
err := e.helper.GuaranteedUpdate(baseKey, &api.RangeAllocation{}, true,
|
|
||||||
func(input runtime.Object) (output runtime.Object, ttl uint64, err error) {
|
|
||||||
existing := input.(*api.RangeAllocation)
|
|
||||||
if len(existing.ResourceVersion) == 0 {
|
|
||||||
return nil, 0, ipallocator.ErrAllocationDisabled
|
|
||||||
}
|
|
||||||
if existing.ResourceVersion != e.last {
|
|
||||||
if err := service.RestoreRange(e.alloc, existing); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if err := fn(); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.last = existing.ResourceVersion
|
|
||||||
service.SnapshotRange(existing, e.alloc)
|
|
||||||
return existing, 0, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return etcderr.InterpretUpdateError(err, "serviceipallocation", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh reloads the ipallocator from etcd.
|
|
||||||
func (e *Etcd) Refresh() error {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
existing := &api.RangeAllocation{}
|
|
||||||
if err := e.helper.ExtractObj(baseKey, existing, false); err != nil {
|
|
||||||
if tools.IsEtcdNotFound(err) {
|
|
||||||
return ipallocator.ErrAllocationDisabled
|
|
||||||
}
|
|
||||||
return etcderr.InterpretGetError(err, "serviceipallocation", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
return service.RestoreRange(e.alloc, existing)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns an api.RangeAllocation that represents the current state in
|
|
||||||
// etcd. If the key does not exist, the object will have an empty ResourceVersion.
|
|
||||||
func (e *Etcd) Get() (*api.RangeAllocation, error) {
|
|
||||||
existing := &api.RangeAllocation{}
|
|
||||||
if err := e.helper.ExtractObj(baseKey, existing, true); err != nil {
|
|
||||||
return nil, etcderr.InterpretGetError(err, "serviceipallocation", "")
|
|
||||||
}
|
|
||||||
return existing, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateOrUpdate attempts to update the current etcd state with the provided
|
|
||||||
// allocation.
|
|
||||||
func (e *Etcd) CreateOrUpdate(snapshot *api.RangeAllocation) error {
|
|
||||||
e.lock.Lock()
|
|
||||||
defer e.lock.Unlock()
|
|
||||||
|
|
||||||
last := ""
|
|
||||||
err := e.helper.GuaranteedUpdate(baseKey, &api.RangeAllocation{}, true,
|
|
||||||
func(input runtime.Object) (output runtime.Object, ttl uint64, err error) {
|
|
||||||
existing := input.(*api.RangeAllocation)
|
|
||||||
switch {
|
|
||||||
case len(snapshot.ResourceVersion) != 0 && len(existing.ResourceVersion) != 0:
|
|
||||||
if snapshot.ResourceVersion != existing.ResourceVersion {
|
|
||||||
return nil, 0, errors.NewConflict("serviceipallocation", "", fmt.Errorf("the provided resource version does not match"))
|
|
||||||
}
|
|
||||||
case len(existing.ResourceVersion) != 0:
|
|
||||||
return nil, 0, errors.NewConflict("serviceipallocation", "", fmt.Errorf("another caller has already initialized the resource"))
|
|
||||||
}
|
|
||||||
last = snapshot.ResourceVersion
|
|
||||||
return snapshot, 0, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return etcderr.InterpretUpdateError(err, "serviceipallocation", "")
|
|
||||||
}
|
|
||||||
err = service.RestoreRange(e.alloc, snapshot)
|
|
||||||
if err == nil {
|
|
||||||
e.last = last
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/go-etcd/etcd"
|
"github.com/coreos/go-etcd/etcd"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
|
||||||
|
allocator_etcd "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator/etcd"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
|
||||||
@@ -37,15 +40,22 @@ func newHelper(t *testing.T) (*tools.FakeEtcdClient, tools.EtcdHelper) {
|
|||||||
return fakeEtcdClient, helper
|
return fakeEtcdClient, helper
|
||||||
}
|
}
|
||||||
|
|
||||||
func newStorage(t *testing.T) (ipallocator.Interface, *ipallocator.Range, *tools.FakeEtcdClient) {
|
func newStorage(t *testing.T) (ipallocator.Interface, allocator.Interface, *tools.FakeEtcdClient) {
|
||||||
fakeEtcdClient, h := newHelper(t)
|
fakeEtcdClient, h := newHelper(t)
|
||||||
_, cidr, err := net.ParseCIDR("192.168.1.0/24")
|
_, cidr, err := net.ParseCIDR("192.168.1.0/24")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
r := ipallocator.NewCIDRRange(cidr)
|
|
||||||
storage := NewEtcd(r, h)
|
var backing allocator.Interface
|
||||||
return storage, r, fakeEtcdClient
|
storage := ipallocator.NewAllocatorCIDRRange(cidr, func(max int, rangeSpec string) allocator.Interface {
|
||||||
|
mem := allocator.NewAllocationMap(max, rangeSpec)
|
||||||
|
backing = mem
|
||||||
|
etcd := allocator_etcd.NewEtcd(mem, "/ranges/serviceips", "serviceipallocation", h)
|
||||||
|
return etcd
|
||||||
|
})
|
||||||
|
|
||||||
|
return storage, backing, fakeEtcdClient
|
||||||
}
|
}
|
||||||
|
|
||||||
func key() string {
|
func key() string {
|
||||||
@@ -56,7 +66,7 @@ func key() string {
|
|||||||
func TestEmpty(t *testing.T) {
|
func TestEmpty(t *testing.T) {
|
||||||
storage, _, ecli := newStorage(t)
|
storage, _, ecli := newStorage(t)
|
||||||
ecli.ExpectNotFoundGet(key())
|
ecli.ExpectNotFoundGet(key())
|
||||||
if err := storage.Allocate(net.ParseIP("192.168.1.2")); err != ipallocator.ErrAllocationDisabled {
|
if err := storage.Allocate(net.ParseIP("192.168.1.2")); fmt.Sprintf("%v", err) != "cannot allocate resources of type serviceipallocation at this time" {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,17 +95,19 @@ func initialObject(ecli *tools.FakeEtcdClient) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
_, cidr, _ := net.ParseCIDR("192.168.1.0/24")
|
|
||||||
|
|
||||||
storage, r, ecli := newStorage(t)
|
storage, r, ecli := newStorage(t)
|
||||||
initialObject(ecli)
|
initialObject(ecli)
|
||||||
|
|
||||||
if err := storage.Allocate(net.ParseIP("192.168.1.2")); err != nil {
|
if err := storage.Allocate(net.ParseIP("192.168.1.2")); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := r.Allocate(net.ParseIP("192.168.1.2")); err != ipallocator.ErrAllocated {
|
ok, err := r.Allocate(1)
|
||||||
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if ok {
|
||||||
|
t.Fatal("Expected allocation to fail")
|
||||||
|
}
|
||||||
if err := storage.Allocate(net.ParseIP("192.168.1.2")); err != ipallocator.ErrAllocated {
|
if err := storage.Allocate(net.ParseIP("192.168.1.2")); err != ipallocator.ErrAllocated {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -105,29 +117,4 @@ func TestStore(t *testing.T) {
|
|||||||
t.Fatalf("%s is empty: %#v", key(), obj)
|
t.Fatalf("%s is empty: %#v", key(), obj)
|
||||||
}
|
}
|
||||||
t.Logf("data: %#v", obj.R.Node)
|
t.Logf("data: %#v", obj.R.Node)
|
||||||
|
|
||||||
other := ipallocator.NewCIDRRange(cidr)
|
|
||||||
|
|
||||||
allocation := &api.RangeAllocation{}
|
|
||||||
if err := storage.(*Etcd).helper.ExtractObj(key(), allocation, false); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if allocation.ResourceVersion != "1" {
|
|
||||||
t.Fatalf("%#v", allocation)
|
|
||||||
}
|
|
||||||
if allocation.Range != "192.168.1.0/24" {
|
|
||||||
t.Errorf("unexpected stored Range: %s", allocation.Range)
|
|
||||||
}
|
|
||||||
if err := other.Restore(cidr, allocation.Data); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !other.Has(net.ParseIP("192.168.1.2")) {
|
|
||||||
t.Fatalf("could not restore allocated IP: %#v", other)
|
|
||||||
}
|
|
||||||
|
|
||||||
other = ipallocator.NewCIDRRange(cidr)
|
|
||||||
otherStorage := NewEtcd(other, storage.(*Etcd).helper)
|
|
||||||
if err := otherStorage.Allocate(net.ParseIP("192.168.1.2")); err != ipallocator.ErrAllocated {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
167
pkg/registry/service/portallocator/allocator.go
Normal file
167
pkg/registry/service/portallocator/allocator.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 portallocator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/allocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface manages the allocation of ports out of a range. Interface
|
||||||
|
// should be threadsafe.
|
||||||
|
type Interface interface {
|
||||||
|
Allocate(int) error
|
||||||
|
AllocateNext() (int, error)
|
||||||
|
Release(int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFull = errors.New("range is full")
|
||||||
|
ErrNotInRange = errors.New("provided port is not in the valid range")
|
||||||
|
ErrAllocated = errors.New("provided port is already allocated")
|
||||||
|
ErrMismatchedNetwork = errors.New("the provided port range does not match the current port range")
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortAllocator struct {
|
||||||
|
portRange util.PortRange
|
||||||
|
|
||||||
|
alloc allocator.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// PortAllocator implements Interface and Snapshottable
|
||||||
|
var _ Interface = &PortAllocator{}
|
||||||
|
|
||||||
|
// NewPortAllocatorCustom creates a PortAllocator over a util.PortRange, calling allocatorFactory to construct the backing store.
|
||||||
|
func NewPortAllocatorCustom(pr util.PortRange, allocatorFactory allocator.AllocatorFactory) *PortAllocator {
|
||||||
|
max := pr.Size
|
||||||
|
rangeSpec := pr.String()
|
||||||
|
|
||||||
|
a := &PortAllocator{
|
||||||
|
portRange: pr,
|
||||||
|
}
|
||||||
|
a.alloc = allocatorFactory(max, rangeSpec)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper that wraps NewAllocatorCIDRRange, for creating a range backed by an in-memory store.
|
||||||
|
func NewPortAllocator(pr util.PortRange) *PortAllocator {
|
||||||
|
return NewPortAllocatorCustom(pr, func(max int, rangeSpec string) allocator.Interface {
|
||||||
|
return allocator.NewAllocationMap(max, rangeSpec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free returns the count of port left in the range.
|
||||||
|
func (r *PortAllocator) Free() int {
|
||||||
|
return r.alloc.Free()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate attempts to reserve the provided port. ErrNotInRange or
|
||||||
|
// ErrAllocated will be returned if the port is not valid for this range
|
||||||
|
// or has already been reserved. ErrFull will be returned if there
|
||||||
|
// are no ports left.
|
||||||
|
func (r *PortAllocator) Allocate(port int) error {
|
||||||
|
ok, offset := r.contains(port)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotInRange
|
||||||
|
}
|
||||||
|
|
||||||
|
allocated, err := r.alloc.Allocate(offset)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !allocated {
|
||||||
|
return ErrAllocated
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllocateNext reserves one of the ports from the pool. ErrFull may
|
||||||
|
// be returned if there are no ports left.
|
||||||
|
func (r *PortAllocator) AllocateNext() (int, error) {
|
||||||
|
offset, ok, err := r.alloc.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return 0, ErrFull
|
||||||
|
}
|
||||||
|
return r.portRange.Base + offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases the port back to the pool. Releasing an
|
||||||
|
// unallocated port or a port out of the range is a no-op and
|
||||||
|
// returns no error.
|
||||||
|
func (r *PortAllocator) Release(port int) error {
|
||||||
|
ok, offset := r.contains(port)
|
||||||
|
if !ok {
|
||||||
|
// TODO: log a warning
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.alloc.Release(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has returns true if the provided port is already allocated and a call
|
||||||
|
// to Allocate(port) would fail with ErrAllocated.
|
||||||
|
func (r *PortAllocator) Has(port int) bool {
|
||||||
|
ok, offset := r.contains(port)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.alloc.Has(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot saves the current state of the pool.
|
||||||
|
func (r *PortAllocator) Snapshot(dst *api.RangeAllocation) error {
|
||||||
|
snapshottable, ok := r.alloc.(allocator.Snapshottable)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a snapshottable allocator")
|
||||||
|
}
|
||||||
|
rangeString, data := snapshottable.Snapshot()
|
||||||
|
dst.Range = rangeString
|
||||||
|
dst.Data = data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore restores the pool to the previously captured state. ErrMismatchedNetwork
|
||||||
|
// is returned if the provided port range doesn't exactly match the previous range.
|
||||||
|
func (r *PortAllocator) Restore(pr util.PortRange, data []byte) error {
|
||||||
|
if pr.String() != r.portRange.String() {
|
||||||
|
return ErrMismatchedNetwork
|
||||||
|
}
|
||||||
|
snapshottable, ok := r.alloc.(allocator.Snapshottable)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a snapshottable allocator")
|
||||||
|
}
|
||||||
|
snapshottable.Restore(pr.String(), data)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains returns true and the offset if the port is in the range, and false
|
||||||
|
// and nil otherwise.
|
||||||
|
func (r *PortAllocator) contains(port int) (bool, int) {
|
||||||
|
if !r.portRange.Contains(port) {
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := port - r.portRange.Base
|
||||||
|
return true, offset
|
||||||
|
}
|
||||||
148
pkg/registry/service/portallocator/allocator_test.go
Normal file
148
pkg/registry/service/portallocator/allocator_test.go
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 portallocator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAllocate(t *testing.T) {
|
||||||
|
pr, err := util.ParsePortRange("10000-10200")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := NewPortAllocator(*pr)
|
||||||
|
if f := r.Free(); f != 201 {
|
||||||
|
t.Errorf("unexpected free %d", f)
|
||||||
|
}
|
||||||
|
found := util.NewStringSet()
|
||||||
|
count := 0
|
||||||
|
for r.Free() > 0 {
|
||||||
|
p, err := r.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error @ %d: %v", count, err)
|
||||||
|
}
|
||||||
|
count++
|
||||||
|
if !pr.Contains(p) {
|
||||||
|
t.Fatalf("allocated %s which is outside of %s", p, pr)
|
||||||
|
}
|
||||||
|
if found.Has(strconv.Itoa(p)) {
|
||||||
|
t.Fatalf("allocated %s twice @ %d", p, count)
|
||||||
|
}
|
||||||
|
found.Insert(strconv.Itoa(p))
|
||||||
|
}
|
||||||
|
if _, err := r.AllocateNext(); err != ErrFull {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
released := 10005
|
||||||
|
if err := r.Release(released); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f := r.Free(); f != 1 {
|
||||||
|
t.Errorf("unexpected free %d", f)
|
||||||
|
}
|
||||||
|
p, err := r.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if released != p {
|
||||||
|
t.Errorf("unexpected %s : %s", p, released)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Release(released); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.Allocate(1); err != ErrNotInRange {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.Allocate(10001); err != ErrAllocated {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.Allocate(20000); err != ErrNotInRange {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.Allocate(10201); err != ErrNotInRange {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f := r.Free(); f != 1 {
|
||||||
|
t.Errorf("unexpected free %d", f)
|
||||||
|
}
|
||||||
|
if err := r.Allocate(released); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if f := r.Free(); f != 0 {
|
||||||
|
t.Errorf("unexpected free %d", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSnapshot(t *testing.T) {
|
||||||
|
pr, err := util.ParsePortRange("10000-10200")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := NewPortAllocator(*pr)
|
||||||
|
ports := []int{}
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
port, err := r.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ports = append(ports, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dst api.RangeAllocation
|
||||||
|
err = r.Snapshot(&dst)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pr2, err := util.ParsePortRange(dst.Range)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pr.String() != pr2.String() {
|
||||||
|
t.Fatalf("mismatched networks: %s : %s", pr, pr2)
|
||||||
|
}
|
||||||
|
|
||||||
|
otherPr, err := util.ParsePortRange("200-300")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
other := NewPortAllocator(*otherPr)
|
||||||
|
if err := r.Restore(*otherPr, dst.Data); err != ErrMismatchedNetwork {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
other = NewPortAllocator(*pr2)
|
||||||
|
if err := other.Restore(*pr2, dst.Data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range ports {
|
||||||
|
if !other.Has(n) {
|
||||||
|
t.Errorf("restored range does not have %s", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if other.Free() != r.Free() {
|
||||||
|
t.Errorf("counts do not match: %d", other.Free())
|
||||||
|
}
|
||||||
|
}
|
||||||
114
pkg/registry/service/portallocator/controller/repair.go
Normal file
114
pkg/registry/service/portallocator/controller/repair.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/portallocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// See ipallocator/controller/repair.go; this is a copy for ports.
|
||||||
|
type Repair struct {
|
||||||
|
interval time.Duration
|
||||||
|
registry service.Registry
|
||||||
|
portRange util.PortRange
|
||||||
|
alloc service.RangeRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRepair creates a controller that periodically ensures that all ports are uniquely allocated across the cluster
|
||||||
|
// and generates informational warnings for a cluster that is not in sync.
|
||||||
|
func NewRepair(interval time.Duration, registry service.Registry, portRange util.PortRange, alloc service.RangeRegistry) *Repair {
|
||||||
|
return &Repair{
|
||||||
|
interval: interval,
|
||||||
|
registry: registry,
|
||||||
|
portRange: portRange,
|
||||||
|
alloc: alloc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunUntil starts the controller until the provided ch is closed.
|
||||||
|
func (c *Repair) RunUntil(ch chan struct{}) {
|
||||||
|
util.Until(func() {
|
||||||
|
if err := c.RunOnce(); err != nil {
|
||||||
|
util.HandleError(err)
|
||||||
|
}
|
||||||
|
}, c.interval, ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunOnce verifies the state of the port allocations and returns an error if an unrecoverable problem occurs.
|
||||||
|
func (c *Repair) RunOnce() error {
|
||||||
|
// TODO: (per smarterclayton) if Get() or ListServices() is a weak consistency read,
|
||||||
|
// or if they are executed against different leaders,
|
||||||
|
// the ordering guarantee required to ensure no port is allocated twice is violated.
|
||||||
|
// ListServices must return a ResourceVersion higher than the etcd index Get triggers,
|
||||||
|
// and the release code must not release services that have had ports allocated but not yet been created
|
||||||
|
// See #8295
|
||||||
|
|
||||||
|
latest, err := c.alloc.Get()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to refresh the port block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := api.WithNamespace(api.NewDefaultContext(), api.NamespaceAll)
|
||||||
|
list, err := c.registry.ListServices(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to refresh the port block: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := portallocator.NewPortAllocator(c.portRange)
|
||||||
|
for i := range list.Items {
|
||||||
|
svc := &list.Items[i]
|
||||||
|
ports := service.CollectServiceNodePorts(svc)
|
||||||
|
if len(ports) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, port := range ports {
|
||||||
|
switch err := r.Allocate(port); err {
|
||||||
|
case nil:
|
||||||
|
case portallocator.ErrAllocated:
|
||||||
|
// TODO: send event
|
||||||
|
// port is broken, reallocate
|
||||||
|
util.HandleError(fmt.Errorf("the port %d for service %s/%s was assigned to multiple services; please recreate", port, svc.Name, svc.Namespace))
|
||||||
|
case portallocator.ErrNotInRange:
|
||||||
|
// TODO: send event
|
||||||
|
// port is broken, reallocate
|
||||||
|
util.HandleError(fmt.Errorf("the port %d for service %s/%s is not within the port range %v; please recreate", port, svc.Name, svc.Namespace, c.portRange))
|
||||||
|
case portallocator.ErrFull:
|
||||||
|
// TODO: send event
|
||||||
|
return fmt.Errorf("the port range %v is full; you must widen the port range in order to create new services", c.portRange)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unable to allocate port %d for service %s/%s due to an unknown error, exiting: %v", port, svc.Name, svc.Namespace, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.Snapshot(latest)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to persist the updated port allocations: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.alloc.CreateOrUpdate(latest); err != nil {
|
||||||
|
return fmt.Errorf("unable to persist the updated port allocations: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
117
pkg/registry/service/portallocator/operation.go
Normal file
117
pkg/registry/service/portallocator/operation.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors 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 portallocator
|
||||||
|
|
||||||
|
// Encapsulates the semantics of a port allocation 'transaction':
|
||||||
|
// It is better to leak ports than to double-allocate them,
|
||||||
|
// so we allocate immediately, but defer release.
|
||||||
|
// On commit we best-effort release the deferred releases.
|
||||||
|
// On rollback we best-effort release any allocations we did.
|
||||||
|
//
|
||||||
|
// Pattern for use:
|
||||||
|
// op := StartPortAllocationOperation(...)
|
||||||
|
// defer op.Finish
|
||||||
|
// ...
|
||||||
|
// write(updatedOwner)
|
||||||
|
/// op.Commit()
|
||||||
|
type portAllocationOperation struct {
|
||||||
|
pa Interface
|
||||||
|
allocated []int
|
||||||
|
releaseDeferred []int
|
||||||
|
shouldRollback bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a portAllocationOperation, tracking a set of allocations & releases
|
||||||
|
func StartOperation(pa Interface) *portAllocationOperation {
|
||||||
|
op := &portAllocationOperation{}
|
||||||
|
op.pa = pa
|
||||||
|
op.allocated = []int{}
|
||||||
|
op.releaseDeferred = []int{}
|
||||||
|
op.shouldRollback = true
|
||||||
|
return op
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will rollback unless marked as shouldRollback = false by a Commit(). Call from a defer block
|
||||||
|
func (op *portAllocationOperation) Finish() {
|
||||||
|
if op.shouldRollback {
|
||||||
|
op.Rollback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Try to) undo any operations we did
|
||||||
|
func (op *portAllocationOperation) Rollback() []error {
|
||||||
|
errors := []error{}
|
||||||
|
|
||||||
|
for _, allocated := range op.allocated {
|
||||||
|
err := op.pa.Release(allocated)
|
||||||
|
if err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// (Try to) perform any deferred operations.
|
||||||
|
// Note that even if this fails, we don't rollback; we always want to err on the side of over-allocation,
|
||||||
|
// and Commit should be called _after_ the owner is written
|
||||||
|
func (op *portAllocationOperation) Commit() []error {
|
||||||
|
errors := []error{}
|
||||||
|
|
||||||
|
for _, release := range op.releaseDeferred {
|
||||||
|
err := op.pa.Release(release)
|
||||||
|
if err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even on error, we don't rollback
|
||||||
|
// Problems should be fixed by an eventual reconciliation / restart
|
||||||
|
op.shouldRollback = false
|
||||||
|
|
||||||
|
if len(errors) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocates a port, and record it for future rollback
|
||||||
|
func (op *portAllocationOperation) Allocate(port int) error {
|
||||||
|
err := op.pa.Allocate(port)
|
||||||
|
if err == nil {
|
||||||
|
op.allocated = append(op.allocated, port)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocates a port, and record it for future rollback
|
||||||
|
func (op *portAllocationOperation) AllocateNext() (int, error) {
|
||||||
|
port, err := op.pa.AllocateNext()
|
||||||
|
if err == nil {
|
||||||
|
op.allocated = append(op.allocated, port)
|
||||||
|
}
|
||||||
|
return port, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marks a port so that it will be released if this operation Commits
|
||||||
|
func (op *portAllocationOperation) ReleaseDeferred(port int) {
|
||||||
|
op.releaseDeferred = append(op.releaseDeferred, port)
|
||||||
|
}
|
||||||
@@ -17,12 +17,9 @@ limitations under the License.
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,8 +33,9 @@ type Registry interface {
|
|||||||
WatchServices(ctx api.Context, labels labels.Selector, fields fields.Selector, resourceVersion string) (watch.Interface, error)
|
WatchServices(ctx api.Context, labels labels.Selector, fields fields.Selector, resourceVersion string) (watch.Interface, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPRegistry is a registry that can retrieve or persist a RangeAllocation object.
|
// TODO: Move to a general location (as other components may need allocation in future; it's not service specific)
|
||||||
type IPRegistry interface {
|
// RangeRegistry is a registry that can retrieve or persist a RangeAllocation object.
|
||||||
|
type RangeRegistry interface {
|
||||||
// Get returns the latest allocation, an empty object if no allocation has been made,
|
// Get returns the latest allocation, an empty object if no allocation has been made,
|
||||||
// or an error if the allocation could not be retrieved.
|
// or an error if the allocation could not be retrieved.
|
||||||
Get() (*api.RangeAllocation, error)
|
Get() (*api.RangeAllocation, error)
|
||||||
@@ -45,19 +43,3 @@ type IPRegistry interface {
|
|||||||
// has occured since the item was last created.
|
// has occured since the item was last created.
|
||||||
CreateOrUpdate(*api.RangeAllocation) error
|
CreateOrUpdate(*api.RangeAllocation) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestoreRange updates a snapshottable ipallocator from a RangeAllocation
|
|
||||||
func RestoreRange(dst ipallocator.Snapshottable, src *api.RangeAllocation) error {
|
|
||||||
_, network, err := net.ParseCIDR(src.Range)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return dst.Restore(network, src.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SnapshotRange updates a RangeAllocation to match a snapshottable ipallocator
|
|
||||||
func SnapshotRange(dst *api.RangeAllocation, src ipallocator.Snapshottable) {
|
|
||||||
network, data := src.Snapshot()
|
|
||||||
dst.Range = network.String()
|
|
||||||
dst.Data = data
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/endpoint"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/minion"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/portallocator"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util/fielderrors"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/watch"
|
||||||
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// REST adapts a service registry into apiserver's RESTStorage model.
|
// REST adapts a service registry into apiserver's RESTStorage model.
|
||||||
@@ -45,17 +47,19 @@ type REST struct {
|
|||||||
machines minion.Registry
|
machines minion.Registry
|
||||||
endpoints endpoint.Registry
|
endpoints endpoint.Registry
|
||||||
portals ipallocator.Interface
|
portals ipallocator.Interface
|
||||||
|
serviceNodePorts portallocator.Interface
|
||||||
clusterName string
|
clusterName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStorage returns a new REST.
|
// NewStorage returns a new REST.
|
||||||
func NewStorage(registry Registry, machines minion.Registry, endpoints endpoint.Registry, portals ipallocator.Interface,
|
func NewStorage(registry Registry, machines minion.Registry, endpoints endpoint.Registry, portals ipallocator.Interface,
|
||||||
clusterName string) *REST {
|
serviceNodePorts portallocator.Interface, clusterName string) *REST {
|
||||||
return &REST{
|
return &REST{
|
||||||
registry: registry,
|
registry: registry,
|
||||||
machines: machines,
|
machines: machines,
|
||||||
endpoints: endpoints,
|
endpoints: endpoints,
|
||||||
portals: portals,
|
portals: portals,
|
||||||
|
serviceNodePorts: serviceNodePorts,
|
||||||
clusterName: clusterName,
|
clusterName: clusterName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +80,9 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
nodePortOp := portallocator.StartOperation(rs.serviceNodePorts)
|
||||||
|
defer nodePortOp.Finish()
|
||||||
|
|
||||||
if api.IsServiceIPRequested(service) {
|
if api.IsServiceIPRequested(service) {
|
||||||
// Allocate next available.
|
// Allocate next available.
|
||||||
ip, err := rs.portals.AllocateNext()
|
ip, err := rs.portals.AllocateNext()
|
||||||
@@ -94,12 +101,37 @@ func (rs *REST) Create(ctx api.Context, obj runtime.Object) (runtime.Object, err
|
|||||||
releaseServiceIP = true
|
releaseServiceIP = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assignNodePorts := shouldAssignNodePorts(service)
|
||||||
|
for i := range service.Spec.Ports {
|
||||||
|
servicePort := &service.Spec.Ports[i]
|
||||||
|
if servicePort.NodePort != 0 {
|
||||||
|
err := nodePortOp.Allocate(servicePort.NodePort)
|
||||||
|
if err != nil {
|
||||||
|
el := fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("nodePort", servicePort.NodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports")
|
||||||
|
return nil, errors.NewInvalid("Service", service.Name, el)
|
||||||
|
}
|
||||||
|
} else if assignNodePorts {
|
||||||
|
nodePort, err := nodePortOp.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
el := fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("nodePort", servicePort.NodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports")
|
||||||
|
return nil, errors.NewInvalid("Service", service.Name, el)
|
||||||
|
}
|
||||||
|
servicePort.NodePort = nodePort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out, err := rs.registry.CreateService(ctx, service)
|
out, err := rs.registry.CreateService(ctx, service)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = rest.CheckGeneratedNameError(rest.Services, err, service)
|
err = rest.CheckGeneratedNameError(rest.Services, err, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
el := nodePortOp.Commit()
|
||||||
|
if el != nil {
|
||||||
|
// these should be caught by an eventual reconciliation / restart
|
||||||
|
glog.Errorf("error(s) committing service node-ports changes: %v", el)
|
||||||
|
}
|
||||||
|
|
||||||
releaseServiceIP = false
|
releaseServiceIP = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,10 +143,25 @@ func (rs *REST) Delete(ctx api.Context, id string) (runtime.Object, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = rs.registry.DeleteService(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if api.IsServiceIPSet(service) {
|
if api.IsServiceIPSet(service) {
|
||||||
rs.portals.Release(net.ParseIP(service.Spec.PortalIP))
|
rs.portals.Release(net.ParseIP(service.Spec.PortalIP))
|
||||||
}
|
}
|
||||||
return &api.Status{Status: api.StatusSuccess}, rs.registry.DeleteService(ctx, id)
|
|
||||||
|
for _, nodePort := range CollectServiceNodePorts(service) {
|
||||||
|
err := rs.serviceNodePorts.Release(nodePort)
|
||||||
|
if err != nil {
|
||||||
|
// these should be caught by an eventual reconciliation / restart
|
||||||
|
glog.Errorf("Error releasing service %s node port %d: %v", service.Name, nodePort, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &api.Status{Status: api.StatusSuccess}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) {
|
func (rs *REST) Get(ctx api.Context, id string) (runtime.Object, error) {
|
||||||
@@ -170,7 +217,70 @@ func (rs *REST) Update(ctx api.Context, obj runtime.Object) (runtime.Object, boo
|
|||||||
if errs := validation.ValidateServiceUpdate(oldService, service); len(errs) > 0 {
|
if errs := validation.ValidateServiceUpdate(oldService, service); len(errs) > 0 {
|
||||||
return nil, false, errors.NewInvalid("service", service.Name, errs)
|
return nil, false, errors.NewInvalid("service", service.Name, errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nodePortOp := portallocator.StartOperation(rs.serviceNodePorts)
|
||||||
|
defer nodePortOp.Finish()
|
||||||
|
|
||||||
|
assignNodePorts := shouldAssignNodePorts(service)
|
||||||
|
|
||||||
|
oldNodePorts := CollectServiceNodePorts(oldService)
|
||||||
|
|
||||||
|
newNodePorts := []int{}
|
||||||
|
if assignNodePorts {
|
||||||
|
for i := range service.Spec.Ports {
|
||||||
|
servicePort := &service.Spec.Ports[i]
|
||||||
|
nodePort := servicePort.NodePort
|
||||||
|
if nodePort != 0 {
|
||||||
|
if !contains(oldNodePorts, nodePort) {
|
||||||
|
err := nodePortOp.Allocate(nodePort)
|
||||||
|
if err != nil {
|
||||||
|
el := fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("nodePort", nodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports")
|
||||||
|
return nil, false, errors.NewInvalid("Service", service.Name, el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nodePort, err = nodePortOp.AllocateNext()
|
||||||
|
if err != nil {
|
||||||
|
el := fielderrors.ValidationErrorList{fielderrors.NewFieldInvalid("nodePort", nodePort, err.Error())}.PrefixIndex(i).Prefix("spec.ports")
|
||||||
|
return nil, false, errors.NewInvalid("Service", service.Name, el)
|
||||||
|
}
|
||||||
|
servicePort.NodePort = nodePort
|
||||||
|
}
|
||||||
|
// Detect duplicate node ports; this should have been caught by validation, so we panic
|
||||||
|
if contains(newNodePorts, nodePort) {
|
||||||
|
panic("duplicate node port")
|
||||||
|
}
|
||||||
|
newNodePorts = append(newNodePorts, nodePort)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validate should have validated that nodePort == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// The comparison loops are O(N^2), but we don't expect N to be huge
|
||||||
|
// (there's a hard-limit at 2^16, because they're ports; and even 4 ports would be a lot)
|
||||||
|
for _, oldNodePort := range oldNodePorts {
|
||||||
|
if !contains(newNodePorts, oldNodePort) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nodePortOp.ReleaseDeferred(oldNodePort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any LoadBalancerStatus now if Type != LoadBalancer;
|
||||||
|
// although loadbalancer delete is actually asynchronous, we don't need to expose the user to that complexity.
|
||||||
|
if service.Spec.Type != api.ServiceTypeLoadBalancer {
|
||||||
|
service.Status.LoadBalancer = api.LoadBalancerStatus{}
|
||||||
|
}
|
||||||
|
|
||||||
out, err := rs.registry.UpdateService(ctx, service)
|
out, err := rs.registry.UpdateService(ctx, service)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
el := nodePortOp.Commit()
|
||||||
|
if el != nil {
|
||||||
|
// problems should be fixed by an eventual reconciliation / restart
|
||||||
|
glog.Errorf("error(s) committing NodePorts changes: %v", el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return out, false, err
|
return out, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,3 +322,39 @@ func (rs *REST) ResourceLocation(ctx api.Context, id string) (*url.URL, http.Rou
|
|||||||
}
|
}
|
||||||
return nil, nil, fmt.Errorf("no endpoints available for %q", id)
|
return nil, nil, fmt.Errorf("no endpoints available for %q", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is O(N), but we expect haystack to be small;
|
||||||
|
// so small that we expect a linear search to be faster
|
||||||
|
func contains(haystack []int, needle int) bool {
|
||||||
|
for _, v := range haystack {
|
||||||
|
if v == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CollectServiceNodePorts(service *api.Service) []int {
|
||||||
|
servicePorts := []int{}
|
||||||
|
for i := range service.Spec.Ports {
|
||||||
|
servicePort := &service.Spec.Ports[i]
|
||||||
|
if servicePort.NodePort != 0 {
|
||||||
|
servicePorts = append(servicePorts, servicePort.NodePort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return servicePorts
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldAssignNodePorts(service *api.Service) bool {
|
||||||
|
switch service.Spec.Type {
|
||||||
|
case api.ServiceTypeLoadBalancer:
|
||||||
|
return true
|
||||||
|
case api.ServiceTypeNodePort:
|
||||||
|
return true
|
||||||
|
case api.ServiceTypeClusterIP:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
glog.Errorf("Unknown service type: %v", service.Spec.Type)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import (
|
|||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/registrytest"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/ipallocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/registry/service/portallocator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewTestREST(t *testing.T, endpoints *api.EndpointsList) (*REST, *registrytest.ServiceRegistry) {
|
func NewTestREST(t *testing.T, endpoints *api.EndpointsList) (*REST, *registrytest.ServiceRegistry) {
|
||||||
@@ -40,7 +42,12 @@ func NewTestREST(t *testing.T, endpoints *api.EndpointsList) (*REST, *registryte
|
|||||||
}
|
}
|
||||||
nodeRegistry := registrytest.NewMinionRegistry(machines, api.NodeResources{})
|
nodeRegistry := registrytest.NewMinionRegistry(machines, api.NodeResources{})
|
||||||
r := ipallocator.NewCIDRRange(makeIPNet(t))
|
r := ipallocator.NewCIDRRange(makeIPNet(t))
|
||||||
storage := NewStorage(registry, nodeRegistry, endpointRegistry, r, "kubernetes")
|
|
||||||
|
portRange := util.PortRange{Base: 30000, Size: 1000}
|
||||||
|
portAllocator := portallocator.NewPortAllocator(portRange)
|
||||||
|
|
||||||
|
storage := NewStorage(registry, nodeRegistry, endpointRegistry, r, portAllocator, "kubernetes")
|
||||||
|
|
||||||
return storage, registry
|
return storage, registry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +75,7 @@ func TestServiceRegistryCreate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -109,6 +117,7 @@ func TestServiceStorageValidatesCreate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -120,6 +129,7 @@ func TestServiceStorageValidatesCreate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
}},
|
}},
|
||||||
@@ -162,6 +172,7 @@ func TestServiceRegistryUpdate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz2"},
|
Selector: map[string]string{"bar": "baz2"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -205,6 +216,7 @@ func TestServiceStorageValidatesUpdate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -216,6 +228,7 @@ func TestServiceStorageValidatesUpdate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"ThisSelectorFailsValidation": "ok"},
|
Selector: map[string]string{"ThisSelectorFailsValidation": "ok"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -241,8 +254,8 @@ func TestServiceRegistryExternalService(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"},
|
||||||
CreateExternalLoadBalancer: true,
|
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -270,6 +283,7 @@ func TestServiceRegistryDelete(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -290,8 +304,8 @@ func TestServiceRegistryDeleteExternal(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"},
|
||||||
CreateExternalLoadBalancer: true,
|
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -314,8 +328,8 @@ func TestServiceRegistryUpdateExternalService(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"},
|
||||||
CreateExternalLoadBalancer: false,
|
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -328,7 +342,7 @@ func TestServiceRegistryUpdateExternalService(t *testing.T) {
|
|||||||
|
|
||||||
// Modify load balancer to be external.
|
// Modify load balancer to be external.
|
||||||
svc2 := deepCloneService(svc1)
|
svc2 := deepCloneService(svc1)
|
||||||
svc2.Spec.CreateExternalLoadBalancer = true
|
svc2.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
if _, _, err := storage.Update(ctx, svc2); err != nil {
|
if _, _, err := storage.Update(ctx, svc2); err != nil {
|
||||||
t.Fatalf("Unexpected error: %v", err)
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -350,8 +364,8 @@ func TestServiceRegistryUpdateMultiPortExternalService(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"},
|
||||||
CreateExternalLoadBalancer: true,
|
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Name: "p",
|
Name: "p",
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
@@ -491,6 +505,7 @@ func TestServiceRegistryIPAllocation(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -512,6 +527,7 @@ func TestServiceRegistryIPAllocation(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -541,6 +557,7 @@ func TestServiceRegistryIPAllocation(t *testing.T) {
|
|||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
PortalIP: testIP,
|
PortalIP: testIP,
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -566,6 +583,7 @@ func TestServiceRegistryIPReallocation(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -582,13 +600,17 @@ func TestServiceRegistryIPReallocation(t *testing.T) {
|
|||||||
t.Errorf("Unexpected PortalIP: %s", created_service_1.Spec.PortalIP)
|
t.Errorf("Unexpected PortalIP: %s", created_service_1.Spec.PortalIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
rest.Delete(ctx, created_service_1.Name)
|
_, err := rest.Delete(ctx, created_service_1.Name)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error deleting service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
svc2 := &api.Service{
|
svc2 := &api.Service{
|
||||||
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"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -614,6 +636,7 @@ func TestServiceRegistryIPUpdate(t *testing.T) {
|
|||||||
Spec: api.ServiceSpec{
|
Spec: api.ServiceSpec{
|
||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -649,15 +672,15 @@ func TestServiceRegistryIPUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServiceRegistryIPExternalLoadBalancer(t *testing.T) {
|
func TestServiceRegistryIPLoadBalancer(t *testing.T) {
|
||||||
rest, _ := NewTestREST(t, nil)
|
rest, _ := NewTestREST(t, nil)
|
||||||
|
|
||||||
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"},
|
||||||
CreateExternalLoadBalancer: true,
|
|
||||||
SessionAffinity: api.ServiceAffinityNone,
|
SessionAffinity: api.ServiceAffinityNone,
|
||||||
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -730,6 +753,7 @@ func TestCreate(t *testing.T) {
|
|||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
PortalIP: "None",
|
PortalIP: "None",
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
@@ -746,6 +770,7 @@ func TestCreate(t *testing.T) {
|
|||||||
Selector: map[string]string{"bar": "baz"},
|
Selector: map[string]string{"bar": "baz"},
|
||||||
PortalIP: "invalid",
|
PortalIP: "invalid",
|
||||||
SessionAffinity: "None",
|
SessionAffinity: "None",
|
||||||
|
Type: api.ServiceTypeClusterIP,
|
||||||
Ports: []api.ServicePort{{
|
Ports: []api.ServicePort{{
|
||||||
Port: 6502,
|
Port: 6502,
|
||||||
Protocol: api.ProtocolTCP,
|
Protocol: api.ProtocolTCP,
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ import (
|
|||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RulePosition string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Prepend RulePosition = "-I"
|
||||||
|
Append RulePosition = "-A"
|
||||||
|
)
|
||||||
|
|
||||||
// An injectable interface for running iptables commands. Implementations must be goroutine-safe.
|
// An injectable interface for running iptables commands. Implementations must be goroutine-safe.
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
// EnsureChain checks if the specified chain exists and, if not, creates it. If the chain existed, return true.
|
// EnsureChain checks if the specified chain exists and, if not, creates it. If the chain existed, return true.
|
||||||
@@ -37,7 +44,7 @@ type Interface interface {
|
|||||||
// DeleteChain deletes the specified chain. If the chain did not exist, return error.
|
// DeleteChain deletes the specified chain. If the chain did not exist, return error.
|
||||||
DeleteChain(table Table, chain Chain) error
|
DeleteChain(table Table, chain Chain) error
|
||||||
// EnsureRule checks if the specified rule is present and, if not, creates it. If the rule existed, return true.
|
// EnsureRule checks if the specified rule is present and, if not, creates it. If the rule existed, return true.
|
||||||
EnsureRule(table Table, chain Chain, args ...string) (bool, error)
|
EnsureRule(position RulePosition, table Table, chain Chain, args ...string) (bool, error)
|
||||||
// DeleteRule checks if the specified rule is present and, if so, deletes it.
|
// DeleteRule checks if the specified rule is present and, if so, deletes it.
|
||||||
DeleteRule(table Table, chain Chain, args ...string) error
|
DeleteRule(table Table, chain Chain, args ...string) error
|
||||||
// IsIpv6 returns true if this is managing ipv6 tables
|
// IsIpv6 returns true if this is managing ipv6 tables
|
||||||
@@ -126,7 +133,7 @@ func (runner *runner) DeleteChain(table Table, chain Chain) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EnsureRule is part of Interface.
|
// EnsureRule is part of Interface.
|
||||||
func (runner *runner) EnsureRule(table Table, chain Chain, args ...string) (bool, error) {
|
func (runner *runner) EnsureRule(position RulePosition, table Table, chain Chain, args ...string) (bool, error) {
|
||||||
fullArgs := makeFullArgs(table, chain, args...)
|
fullArgs := makeFullArgs(table, chain, args...)
|
||||||
|
|
||||||
runner.mu.Lock()
|
runner.mu.Lock()
|
||||||
@@ -139,7 +146,7 @@ func (runner *runner) EnsureRule(table Table, chain Chain, args ...string) (bool
|
|||||||
if exists {
|
if exists {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
out, err := runner.run(opAppendRule, fullArgs)
|
out, err := runner.run(operation(position), fullArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error appending rule: %v: %s", err, out)
|
return false, fmt.Errorf("error appending rule: %v: %s", err, out)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ func TestEnsureRuleAlreadyExists(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
runner := New(&fexec, ProtocolIpv4)
|
runner := New(&fexec, ProtocolIpv4)
|
||||||
exists, err := runner.EnsureRule(TableNAT, ChainOutput, "abc", "123")
|
exists, err := runner.EnsureRule(Append, TableNAT, ChainOutput, "abc", "123")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected success, got %v", err)
|
t.Errorf("expected success, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -212,7 +212,7 @@ func TestEnsureRuleNew(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
runner := New(&fexec, ProtocolIpv4)
|
runner := New(&fexec, ProtocolIpv4)
|
||||||
exists, err := runner.EnsureRule(TableNAT, ChainOutput, "abc", "123")
|
exists, err := runner.EnsureRule(Append, TableNAT, ChainOutput, "abc", "123")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected success, got %v", err)
|
t.Errorf("expected success, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ func TestEnsureRuleErrorChecking(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
runner := New(&fexec, ProtocolIpv4)
|
runner := New(&fexec, ProtocolIpv4)
|
||||||
_, err := runner.EnsureRule(TableNAT, ChainOutput, "abc", "123")
|
_, err := runner.EnsureRule(Append, TableNAT, ChainOutput, "abc", "123")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected failure")
|
t.Errorf("expected failure")
|
||||||
}
|
}
|
||||||
@@ -275,7 +275,7 @@ func TestEnsureRuleErrorCreating(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
runner := New(&fexec, ProtocolIpv4)
|
runner := New(&fexec, ProtocolIpv4)
|
||||||
_, err := runner.EnsureRule(TableNAT, ChainOutput, "abc", "123")
|
_, err := runner.EnsureRule(Append, TableNAT, ChainOutput, "abc", "123")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("expected failure")
|
t.Errorf("expected failure")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,27 @@ package e2e
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// This should match whatever the default/configured range is
|
||||||
|
var ServiceNodePortRange = util.PortRange{Base: 30000, Size: 2767}
|
||||||
|
|
||||||
var _ = Describe("Services", func() {
|
var _ = Describe("Services", func() {
|
||||||
var c *client.Client
|
var c *client.Client
|
||||||
// Use these in tests. They're unique for each test to prevent name collisions.
|
// Use these in tests. They're unique for each test to prevent name collisions.
|
||||||
@@ -252,31 +259,76 @@ var _ = Describe("Services", func() {
|
|||||||
}, 240.0)
|
}, 240.0)
|
||||||
|
|
||||||
It("should be able to create a functioning external load balancer", func() {
|
It("should be able to create a functioning external load balancer", func() {
|
||||||
if !providerIs("gce", "gke") {
|
if !providerIs("gce", "gke", "aws") {
|
||||||
By(fmt.Sprintf("Skipping service external load balancer test; uses createExternalLoadBalancer, a (gce|gke) feature"))
|
By(fmt.Sprintf("Skipping service external load balancer test; uses ServiceTypeLoadBalancer, a (gce|gke|aws) feature"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceName := "external-lb-test"
|
serviceName := "external-lb-test"
|
||||||
ns := namespaces[0]
|
ns := namespaces[0]
|
||||||
labels := map[string]string{
|
|
||||||
"key0": "value0",
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
}
|
defer func() {
|
||||||
service := &api.Service{
|
defer GinkgoRecover()
|
||||||
ObjectMeta: api.ObjectMeta{
|
errs := t.Cleanup()
|
||||||
Name: serviceName,
|
if len(errs) != 0 {
|
||||||
},
|
Failf("errors in cleanup: %v", errs)
|
||||||
Spec: api.ServiceSpec{
|
|
||||||
Selector: labels,
|
|
||||||
Ports: []api.ServicePort{{
|
|
||||||
Port: 80,
|
|
||||||
TargetPort: util.NewIntOrStringFromInt(80),
|
|
||||||
}},
|
|
||||||
CreateExternalLoadBalancer: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
|
||||||
By("creating service " + serviceName + " with external load balancer in namespace " + ns)
|
By("creating service " + serviceName + " with external load balancer in namespace " + ns)
|
||||||
|
result, err := t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Wait for the load balancer to be created asynchronously, which is
|
||||||
|
// currently indicated by ingress point(s) being added to the status.
|
||||||
|
result, err = waitForLoadBalancerIngress(c, serviceName, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
if len(result.Status.LoadBalancer.Ingress) != 1 {
|
||||||
|
Failf("got unexpected number (%v) of ingress points for externally load balanced service: %v", result.Status.LoadBalancer.Ingress, result)
|
||||||
|
}
|
||||||
|
ingress := result.Status.LoadBalancer.Ingress[0]
|
||||||
|
if len(result.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for LoadBalancer service: %v", result)
|
||||||
|
}
|
||||||
|
port := result.Spec.Ports[0]
|
||||||
|
if port.NodePort == 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for LoadBalancer service: %v", result)
|
||||||
|
}
|
||||||
|
if !ServiceNodePortRange.Contains(port.NodePort) {
|
||||||
|
Failf("got unexpected (out-of-range) port for LoadBalancer service: %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("creating pod to be part of service " + serviceName)
|
||||||
|
t.CreateWebserverPod()
|
||||||
|
|
||||||
|
By("hitting the pod through the service's NodePort")
|
||||||
|
testReachable(pickMinionIP(c), port.NodePort)
|
||||||
|
|
||||||
|
By("hitting the pod through the service's external load balancer")
|
||||||
|
testLoadBalancerReachable(ingress, 80)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should be able to create a functioning NodePort service", func() {
|
||||||
|
serviceName := "nodeportservice-test"
|
||||||
|
ns := namespaces[0]
|
||||||
|
|
||||||
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
|
defer func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
errs := t.Cleanup()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
Failf("errors in cleanup: %v", errs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with type=NodePort in namespace " + ns)
|
||||||
result, err := c.Services(ns).Create(service)
|
result, err := c.Services(ns).Create(service)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
defer func(ns, serviceName string) { // clean up when we're done
|
defer func(ns, serviceName string) { // clean up when we're done
|
||||||
@@ -285,71 +337,440 @@ var _ = Describe("Services", func() {
|
|||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
}(ns, serviceName)
|
}(ns, serviceName)
|
||||||
|
|
||||||
// Wait for the load balancer to be created asynchronously, which is
|
if len(result.Spec.Ports) != 1 {
|
||||||
// currently indicated by a public IP address being added to the spec.
|
Failf("got unexpected number (%d) of Ports for NodePort service: %v", len(result.Spec.Ports), result)
|
||||||
result, err = waitForPublicIPs(c, serviceName, ns)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
if len(result.Spec.PublicIPs) != 1 {
|
|
||||||
Failf("got unexpected number (%d) of public IPs for externally load balanced service: %v", result.Spec.PublicIPs, result)
|
|
||||||
}
|
}
|
||||||
ip := result.Spec.PublicIPs[0]
|
|
||||||
port := result.Spec.Ports[0].Port
|
|
||||||
|
|
||||||
pod := &api.Pod{
|
nodePort := result.Spec.Ports[0].NodePort
|
||||||
TypeMeta: api.TypeMeta{
|
if nodePort == 0 {
|
||||||
Kind: "Pod",
|
Failf("got unexpected nodePort (%d) on Ports[0] for NodePort service: %v", nodePort, result)
|
||||||
APIVersion: latest.Version,
|
}
|
||||||
},
|
if !ServiceNodePortRange.Contains(nodePort) {
|
||||||
ObjectMeta: api.ObjectMeta{
|
Failf("got unexpected (out-of-range) port for NodePort service: %v", result)
|
||||||
Name: "elb-test-" + string(util.NewUUID()),
|
|
||||||
Labels: labels,
|
|
||||||
},
|
|
||||||
Spec: api.PodSpec{
|
|
||||||
Containers: []api.Container{
|
|
||||||
{
|
|
||||||
Name: "webserver",
|
|
||||||
Image: "gcr.io/google_containers/test-webserver",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
By("creating pod to be part of service " + serviceName)
|
By("creating pod to be part of service " + serviceName)
|
||||||
podClient := c.Pods(ns)
|
t.CreateWebserverPod()
|
||||||
defer func() {
|
|
||||||
By("deleting pod " + pod.Name)
|
|
||||||
defer GinkgoRecover()
|
|
||||||
podClient.Delete(pod.Name, nil)
|
|
||||||
}()
|
|
||||||
if _, err := podClient.Create(pod); err != nil {
|
|
||||||
Failf("Failed to create pod %s: %v", pod.Name, err)
|
|
||||||
}
|
|
||||||
expectNoError(waitForPodRunningInNamespace(c, pod.Name, ns))
|
|
||||||
|
|
||||||
By("hitting the pod through the service's external load balancer")
|
By("hitting the pod through the service's NodePort")
|
||||||
var resp *http.Response
|
ip := pickMinionIP(c)
|
||||||
for t := time.Now(); time.Since(t) < podStartTimeout; time.Sleep(5 * time.Second) {
|
testReachable(ip, nodePort)
|
||||||
resp, err = http.Get(fmt.Sprintf("http://%s:%d", ip, port))
|
})
|
||||||
|
|
||||||
|
It("should be able to change the type and nodeport settings of a service", func() {
|
||||||
|
serviceName := "mutability-service-test"
|
||||||
|
ns := namespaces[0]
|
||||||
|
|
||||||
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
|
defer func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
errs := t.Cleanup()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
Failf("errors in cleanup: %v", errs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with type unspecified in namespace " + t.Namespace)
|
||||||
|
service, err := t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeClusterIP {
|
||||||
|
Failf("got unexpected Spec.Type for default service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for default service: %v", service)
|
||||||
|
}
|
||||||
|
port := service.Spec.Ports[0]
|
||||||
|
if port.NodePort != 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for default service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 0 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for default service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("creating pod to be part of service " + t.ServiceName)
|
||||||
|
t.CreateWebserverPod()
|
||||||
|
|
||||||
|
By("changing service " + serviceName + " to type=NodePort")
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
service, err = c.Services(ns).Update(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeNodePort {
|
||||||
|
Failf("got unexpected Spec.Type for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
port = service.Spec.Ports[0]
|
||||||
|
if port.NodePort == 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
if !ServiceNodePortRange.Contains(port.NodePort) {
|
||||||
|
Failf("got unexpected (out-of-range) port for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 0 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
By("hitting the pod through the service's NodePort")
|
||||||
|
ip := pickMinionIP(c)
|
||||||
|
nodePort1 := port.NodePort // Save for later!
|
||||||
|
testReachable(ip, nodePort1)
|
||||||
|
|
||||||
|
By("changing service " + serviceName + " to type=LoadBalancer")
|
||||||
|
service.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
service, err = c.Services(ns).Update(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
// Wait for the load balancer to be created asynchronously
|
||||||
|
service, err = waitForLoadBalancerIngress(c, serviceName, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeLoadBalancer {
|
||||||
|
Failf("got unexpected Spec.Type for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
port = service.Spec.Ports[0]
|
||||||
|
if port.NodePort != nodePort1 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 1 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
ingress1 := service.Status.LoadBalancer.Ingress[0]
|
||||||
|
if ingress1.IP == "" && ingress1.Hostname == "" {
|
||||||
|
Failf("got unexpected Status.LoadBalancer.Ingresss[0] for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
By("hitting the pod through the service's NodePort")
|
||||||
|
ip = pickMinionIP(c)
|
||||||
|
testReachable(ip, nodePort1)
|
||||||
|
By("hitting the pod through the service's LoadBalancer")
|
||||||
|
testLoadBalancerReachable(ingress1, 80)
|
||||||
|
|
||||||
|
By("changing service " + serviceName + " update NodePort")
|
||||||
|
nodePort2 := nodePort1 - 1
|
||||||
|
if !ServiceNodePortRange.Contains(nodePort2) {
|
||||||
|
//Check for (unlikely) assignment at bottom of range
|
||||||
|
nodePort2 = nodePort1 + 1
|
||||||
|
}
|
||||||
|
service.Spec.Ports[0].NodePort = nodePort2
|
||||||
|
service, err = c.Services(ns).Update(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeLoadBalancer {
|
||||||
|
Failf("got unexpected Spec.Type for updated-NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for updated-NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
port = service.Spec.Ports[0]
|
||||||
|
if port.NodePort != nodePort2 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 1 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
ingress2 := service.Status.LoadBalancer.Ingress[0]
|
||||||
|
// TODO: This is a problem on AWS; we can't just always be changing the LB
|
||||||
|
Expect(ingress1).To(Equal(ingress2))
|
||||||
|
|
||||||
|
By("hitting the pod through the service's updated NodePort")
|
||||||
|
testReachable(ip, nodePort2)
|
||||||
|
By("hitting the pod through the service's LoadBalancer")
|
||||||
|
testLoadBalancerReachable(ingress2, 80)
|
||||||
|
By("checking the old NodePort is closed")
|
||||||
|
testNotReachable(ip, nodePort1)
|
||||||
|
|
||||||
|
By("changing service " + serviceName + " back to type=ClusterIP")
|
||||||
|
service.Spec.Type = api.ServiceTypeClusterIP
|
||||||
|
service, err = c.Services(ns).Update(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeClusterIP {
|
||||||
|
Failf("got unexpected Spec.Type for back-to-ClusterIP service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for back-to-ClusterIP service: %v", service)
|
||||||
|
}
|
||||||
|
port = service.Spec.Ports[0]
|
||||||
|
if port.NodePort != 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for back-to-ClusterIP service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the load balancer to be destroyed asynchronously
|
||||||
|
service, err = waitForLoadBalancerDestroy(c, serviceName, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 0 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for back-to-ClusterIP service: %v", service)
|
||||||
|
}
|
||||||
|
By("checking the NodePort (original) is closed")
|
||||||
|
ip = pickMinionIP(c)
|
||||||
|
testNotReachable(ip, nodePort1)
|
||||||
|
By("checking the NodePort (updated) is closed")
|
||||||
|
ip = pickMinionIP(c)
|
||||||
|
testNotReachable(ip, nodePort2)
|
||||||
|
By("checking the LoadBalancer is closed")
|
||||||
|
testLoadBalancerNotReachable(ingress2, 80)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should release the load balancer when Type goes from LoadBalancer -> NodePort", func() {
|
||||||
|
serviceName := "service-release-lb"
|
||||||
|
ns := namespaces[0]
|
||||||
|
|
||||||
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
|
defer func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
errs := t.Cleanup()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
Failf("errors in cleanup: %v", errs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeLoadBalancer
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with type LoadBalancer")
|
||||||
|
service, err := t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("creating pod to be part of service " + t.ServiceName)
|
||||||
|
t.CreateWebserverPod()
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeLoadBalancer {
|
||||||
|
Failf("got unexpected Spec.Type for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
nodePort := service.Spec.Ports[0].NodePort
|
||||||
|
if nodePort == 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].NodePort for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the load balancer to be created asynchronously
|
||||||
|
service, err = waitForLoadBalancerIngress(c, serviceName, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 1 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
ingress := service.Status.LoadBalancer.Ingress[0]
|
||||||
|
if ingress.IP == "" && ingress.Hostname == "" {
|
||||||
|
Failf("got unexpected Status.LoadBalancer.Ingresss[0] for LoadBalancer service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("hitting the pod through the service's NodePort")
|
||||||
|
ip := pickMinionIP(c)
|
||||||
|
testReachable(ip, nodePort)
|
||||||
|
By("hitting the pod through the service's LoadBalancer")
|
||||||
|
testLoadBalancerReachable(ingress, 80)
|
||||||
|
|
||||||
|
By("changing service " + serviceName + " to type=NodePort")
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
service, err = c.Services(ns).Update(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeNodePort {
|
||||||
|
Failf("got unexpected Spec.Type for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
if service.Spec.Ports[0].NodePort != nodePort {
|
||||||
|
Failf("got unexpected Spec.Ports[0].NodePort for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the load balancer to be created asynchronously
|
||||||
|
service, err = waitForLoadBalancerDestroy(c, serviceName, ns)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) != 0 {
|
||||||
|
Failf("got unexpected len(Status.LoadBalancer.Ingresss) for NodePort service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("hitting the pod through the service's NodePort")
|
||||||
|
testReachable(ip, nodePort)
|
||||||
|
By("checking the LoadBalancer is closed")
|
||||||
|
testLoadBalancerNotReachable(ingress, 80)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should prevent NodePort collisions", func() {
|
||||||
|
serviceName := "nodeport-collision"
|
||||||
|
serviceName2 := serviceName + "2"
|
||||||
|
ns := namespaces[0]
|
||||||
|
|
||||||
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
|
defer func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
errs := t.Cleanup()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
Failf("errors in cleanup: %v", errs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with type NodePort in namespace " + ns)
|
||||||
|
result, err := t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if result.Spec.Type != api.ServiceTypeNodePort {
|
||||||
|
Failf("got unexpected Spec.Type for new service: %v", result)
|
||||||
|
}
|
||||||
|
if len(result.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for new service: %v", result)
|
||||||
|
}
|
||||||
|
port := result.Spec.Ports[0]
|
||||||
|
if port.NodePort == 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for new service: %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with conflicting NodePort")
|
||||||
|
|
||||||
|
service2 := t.BuildServiceSpec()
|
||||||
|
service2.Name = serviceName2
|
||||||
|
service2.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
service2.Spec.Ports[0].NodePort = port.NodePort
|
||||||
|
|
||||||
|
By("creating service " + serviceName2 + " with conflicting NodePort")
|
||||||
|
result2, err := t.CreateService(service2)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
Failf("Created service with conflicting NodePort: %v", result2)
|
||||||
|
}
|
||||||
|
expectedErr := fmt.Sprintf("Service \"%s\" is invalid: spec.ports[0].nodePort: invalid value '%d': provided port is already allocated", serviceName2, port.NodePort)
|
||||||
|
Expect(fmt.Sprintf("%v", err)).To(Equal(expectedErr))
|
||||||
|
|
||||||
|
By("deleting original service " + serviceName + " with type NodePort in namespace " + ns)
|
||||||
|
err = t.DeleteService(serviceName)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("creating service " + serviceName2 + " with no-longer-conflicting NodePort")
|
||||||
|
_, err = t.CreateService(service2)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should check NodePort out-of-range", func() {
|
||||||
|
serviceName := "nodeport-range-test"
|
||||||
|
ns := namespaces[0]
|
||||||
|
|
||||||
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
|
defer func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
errs := t.Cleanup()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
Failf("errors in cleanup: %v", errs)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with type NodePort in namespace " + ns)
|
||||||
|
service, err := t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeNodePort {
|
||||||
|
Failf("got unexpected Spec.Type for new service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for new service: %v", service)
|
||||||
|
}
|
||||||
|
port := service.Spec.Ports[0]
|
||||||
|
if port.NodePort == 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for new service: %v", service)
|
||||||
|
}
|
||||||
|
if !ServiceNodePortRange.Contains(port.NodePort) {
|
||||||
|
Failf("got unexpected (out-of-range) port for new service: %v", service)
|
||||||
|
}
|
||||||
|
|
||||||
|
outOfRangeNodePort := 0
|
||||||
|
for {
|
||||||
|
outOfRangeNodePort = 1 + rand.Intn(65535)
|
||||||
|
if !ServiceNodePortRange.Contains(outOfRangeNodePort) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expect(err).NotTo(HaveOccurred())
|
By(fmt.Sprintf("changing service "+serviceName+" to out-of-range NodePort %d", outOfRangeNodePort))
|
||||||
defer resp.Body.Close()
|
service.Spec.Ports[0].NodePort = outOfRangeNodePort
|
||||||
|
result, err := t.Client.Services(t.Namespace).Update(service)
|
||||||
|
if err == nil {
|
||||||
|
Failf("failed to prevent update of service with out-of-range NodePort: %v", result)
|
||||||
|
}
|
||||||
|
expectedErr := fmt.Sprintf("Service \"%s\" is invalid: spec.ports[0].nodePort: invalid value '%d': provided port is not in the valid range", serviceName, outOfRangeNodePort)
|
||||||
|
Expect(fmt.Sprintf("%v", err)).To(Equal(expectedErr))
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
By("deleting original service " + serviceName)
|
||||||
|
err = t.DeleteService(serviceName)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
Failf("received non-success return status %q trying to access pod through load balancer; got body: %s", resp.Status, string(body))
|
By(fmt.Sprintf("creating service "+serviceName+" with out-of-range NodePort %d", outOfRangeNodePort))
|
||||||
|
service = t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
service.Spec.Ports[0].NodePort = outOfRangeNodePort
|
||||||
|
service, err = t.CreateService(service)
|
||||||
|
if err == nil {
|
||||||
|
Failf("failed to prevent create of service with out-of-range NodePort (%d): %v", outOfRangeNodePort, service)
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(body), "test-webserver") {
|
Expect(fmt.Sprintf("%v", err)).To(Equal(expectedErr))
|
||||||
Failf("received response body without expected substring 'test-webserver': %s", string(body))
|
})
|
||||||
|
|
||||||
|
It("should release NodePorts on delete", func() {
|
||||||
|
serviceName := "nodeport-reuse"
|
||||||
|
ns := namespaces[0]
|
||||||
|
|
||||||
|
t := NewWebserverTest(c, ns, serviceName)
|
||||||
|
defer func() {
|
||||||
|
defer GinkgoRecover()
|
||||||
|
errs := t.Cleanup()
|
||||||
|
if len(errs) != 0 {
|
||||||
|
Failf("errors in cleanup: %v", errs)
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
service := t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
|
||||||
|
By("creating service " + serviceName + " with type NodePort in namespace " + ns)
|
||||||
|
service, err := t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
if service.Spec.Type != api.ServiceTypeNodePort {
|
||||||
|
Failf("got unexpected Spec.Type for new service: %v", service)
|
||||||
|
}
|
||||||
|
if len(service.Spec.Ports) != 1 {
|
||||||
|
Failf("got unexpected len(Spec.Ports) for new service: %v", service)
|
||||||
|
}
|
||||||
|
port := service.Spec.Ports[0]
|
||||||
|
if port.NodePort == 0 {
|
||||||
|
Failf("got unexpected Spec.Ports[0].nodePort for new service: %v", service)
|
||||||
|
}
|
||||||
|
if !ServiceNodePortRange.Contains(port.NodePort) {
|
||||||
|
Failf("got unexpected (out-of-range) port for new service: %v", service)
|
||||||
|
}
|
||||||
|
port1 := port.NodePort
|
||||||
|
|
||||||
|
By("deleting original service " + serviceName)
|
||||||
|
err = t.DeleteService(serviceName)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By(fmt.Sprintf("creating service "+serviceName+" with same NodePort %d", port1))
|
||||||
|
service = t.BuildServiceSpec()
|
||||||
|
service.Spec.Type = api.ServiceTypeNodePort
|
||||||
|
service.Spec.Ports[0].NodePort = port1
|
||||||
|
service, err = t.CreateService(service)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should correctly serve identically named services in different namespaces on different external IP addresses", func() {
|
It("should correctly serve identically named services in different namespaces on different external IP addresses", func() {
|
||||||
if !providerIs("gce", "gke") {
|
if !providerIs("gce", "gke", "aws") {
|
||||||
By(fmt.Sprintf("Skipping service namespace collision test; uses createExternalLoadBalancer, a (gce|gke) feature"))
|
By(fmt.Sprintf("Skipping service namespace collision test; uses ServiceTypeLoadBalancer, a (gce|gke|aws) feature"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,11 +787,11 @@ var _ = Describe("Services", func() {
|
|||||||
Port: 80,
|
Port: 80,
|
||||||
TargetPort: util.NewIntOrStringFromInt(80),
|
TargetPort: util.NewIntOrStringFromInt(80),
|
||||||
}},
|
}},
|
||||||
CreateExternalLoadBalancer: true,
|
Type: api.ServiceTypeLoadBalancer,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
publicIPs := []string{}
|
ingressPoints := []string{}
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range namespaces {
|
||||||
for _, serviceName := range serviceNames {
|
for _, serviceName := range serviceNames {
|
||||||
service.ObjectMeta.Name = serviceName
|
service.ObjectMeta.Name = serviceName
|
||||||
@@ -387,31 +808,55 @@ var _ = Describe("Services", func() {
|
|||||||
}
|
}
|
||||||
for _, namespace := range namespaces {
|
for _, namespace := range namespaces {
|
||||||
for _, serviceName := range serviceNames {
|
for _, serviceName := range serviceNames {
|
||||||
result, err := waitForPublicIPs(c, serviceName, namespace)
|
result, err := waitForLoadBalancerIngress(c, serviceName, namespace)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
publicIPs = append(publicIPs, result.Spec.PublicIPs...) // Save 'em to check uniqueness
|
for i := range result.Status.LoadBalancer.Ingress {
|
||||||
|
ingress := result.Status.LoadBalancer.Ingress[i].IP
|
||||||
|
if ingress == "" {
|
||||||
|
ingress = result.Status.LoadBalancer.Ingress[i].Hostname
|
||||||
|
}
|
||||||
|
ingressPoints = append(ingressPoints, ingress) // Save 'em to check uniqueness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
validateUniqueOrFail(publicIPs)
|
}
|
||||||
|
validateUniqueOrFail(ingressPoints)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
func waitForPublicIPs(c *client.Client, serviceName, namespace string) (*api.Service, error) {
|
func waitForLoadBalancerIngress(c *client.Client, serviceName, namespace string) (*api.Service, error) {
|
||||||
const timeout = 4 * time.Minute
|
const timeout = 4 * time.Minute
|
||||||
var service *api.Service
|
var service *api.Service
|
||||||
By(fmt.Sprintf("waiting up to %v for service %s in namespace %s to have a public IP", timeout, serviceName, namespace))
|
By(fmt.Sprintf("waiting up to %v for service %s in namespace %s to have a LoadBalancer ingress point", timeout, serviceName, namespace))
|
||||||
for start := time.Now(); time.Since(start) < timeout; time.Sleep(5 * time.Second) {
|
for start := time.Now(); time.Since(start) < timeout; time.Sleep(5 * time.Second) {
|
||||||
service, err := c.Services(namespace).Get(serviceName)
|
service, err := c.Services(namespace).Get(serviceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logf("Get service failed, ignoring for 5s: %v", err)
|
Logf("Get service failed, ignoring for 5s: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if len(service.Spec.PublicIPs) > 0 {
|
if len(service.Status.LoadBalancer.Ingress) > 0 {
|
||||||
return service, nil
|
return service, nil
|
||||||
}
|
}
|
||||||
Logf("Waiting for service %s in namespace %s to have a public IP (%v)", serviceName, namespace, time.Since(start))
|
Logf("Waiting for service %s in namespace %s to have a LoadBalancer ingress point (%v)", serviceName, namespace, time.Since(start))
|
||||||
}
|
}
|
||||||
return service, fmt.Errorf("service %s in namespace %s doesn't have a public IP after %.2f seconds", serviceName, namespace, timeout.Seconds())
|
return service, fmt.Errorf("service %s in namespace %s doesn't have a LoadBalancer ingress point after %.2f seconds", serviceName, namespace, timeout.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForLoadBalancerDestroy(c *client.Client, serviceName, namespace string) (*api.Service, error) {
|
||||||
|
const timeout = 4 * time.Minute
|
||||||
|
var service *api.Service
|
||||||
|
By(fmt.Sprintf("waiting up to %v for service %s in namespace %s to have no LoadBalancer ingress points", timeout, serviceName, namespace))
|
||||||
|
for start := time.Now(); time.Since(start) < timeout; time.Sleep(5 * time.Second) {
|
||||||
|
service, err := c.Services(namespace).Get(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
Logf("Get service failed, ignoring for 5s: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(service.Status.LoadBalancer.Ingress) == 0 {
|
||||||
|
return service, nil
|
||||||
|
}
|
||||||
|
Logf("Waiting for service %s in namespace %s to have no LoadBalancer ingress points (%v)", serviceName, namespace, time.Since(start))
|
||||||
|
}
|
||||||
|
return service, fmt.Errorf("service %s in namespace %s still has LoadBalancer ingress points after %.2f seconds", serviceName, namespace, timeout.Seconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUniqueOrFail(s []string) {
|
func validateUniqueOrFail(s []string) {
|
||||||
@@ -524,3 +969,267 @@ func addEndpointPodOrFail(c *client.Client, ns, name string, labels map[string]s
|
|||||||
_, err := c.Pods(ns).Create(pod)
|
_, err := c.Pods(ns).Create(pod)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func collectAddresses(nodes *api.NodeList, addressType api.NodeAddressType) []string {
|
||||||
|
ips := []string{}
|
||||||
|
for i := range nodes.Items {
|
||||||
|
item := &nodes.Items[i]
|
||||||
|
for j := range item.Status.Addresses {
|
||||||
|
nodeAddress := &item.Status.Addresses[j]
|
||||||
|
if nodeAddress.Type == addressType {
|
||||||
|
ips = append(ips, nodeAddress.Address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ips
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMinionPublicIps(c *client.Client) ([]string, error) {
|
||||||
|
nodes, err := c.Nodes().List(labels.Everything(), fields.Everything())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := collectAddresses(nodes, api.NodeExternalIP)
|
||||||
|
if len(ips) == 0 {
|
||||||
|
ips = collectAddresses(nodes, api.NodeLegacyHostIP)
|
||||||
|
}
|
||||||
|
return ips, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickMinionIP(c *client.Client) string {
|
||||||
|
publicIps, err := getMinionPublicIps(c)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
if len(publicIps) == 0 {
|
||||||
|
Failf("got unexpected number (%d) of public IPs", len(publicIps))
|
||||||
|
}
|
||||||
|
ip := publicIps[0]
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadBalancerReachable(ingress api.LoadBalancerIngress, port int) {
|
||||||
|
ip := ingress.IP
|
||||||
|
if ip == "" {
|
||||||
|
ip = ingress.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
testReachable(ip, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadBalancerNotReachable(ingress api.LoadBalancerIngress, port int) {
|
||||||
|
ip := ingress.IP
|
||||||
|
if ip == "" {
|
||||||
|
ip = ingress.Hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
testNotReachable(ip, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testReachable(ip string, port int) {
|
||||||
|
var err error
|
||||||
|
var resp *http.Response
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s:%d", ip, port)
|
||||||
|
if ip == "" {
|
||||||
|
Failf("got empty IP for reachability check", url)
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
Failf("got port==0 for reachability check", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
By(fmt.Sprintf("Checking reachability of %s", url))
|
||||||
|
for t := time.Now(); time.Since(t) < podStartTimeout; time.Sleep(5 * time.Second) {
|
||||||
|
resp, err = httpGetNoConnectionPool(url)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
By(fmt.Sprintf("Got error waiting for reachability of %s: %v", url, err))
|
||||||
|
}
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
Failf("received non-success return status %q trying to access %s; got body: %s", resp.Status, url, string(body))
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(body), "test-webserver") {
|
||||||
|
Failf("received response body without expected substring 'test-webserver': %s", string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNotReachable(ip string, port int) {
|
||||||
|
var err error
|
||||||
|
var resp *http.Response
|
||||||
|
var body []byte
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s:%d", ip, port)
|
||||||
|
if ip == "" {
|
||||||
|
Failf("got empty IP for non-reachability check", url)
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
Failf("got port==0 for non-reachability check", url)
|
||||||
|
}
|
||||||
|
|
||||||
|
for t := time.Now(); time.Since(t) < podStartTimeout; time.Sleep(5 * time.Second) {
|
||||||
|
resp, err = httpGetNoConnectionPool(url)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
body, err = ioutil.ReadAll(resp.Body)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
resp.Body.Close()
|
||||||
|
By(fmt.Sprintf("Got success waiting for non-reachability of %s: %v", url, resp.Status))
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
Failf("able to reach service %s when should no longer have been reachable: %q body=%s", url, resp.Status, string(body))
|
||||||
|
}
|
||||||
|
// TODO: Check type of error
|
||||||
|
By(fmt.Sprintf("Found (expected) error during not-reachability test %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does an HTTP GET, but does not reuse TCP connections
|
||||||
|
// This masks problems where the iptables rule has changed, but we don't see it
|
||||||
|
func httpGetNoConnectionPool(url string) (*http.Response, error) {
|
||||||
|
tr := &http.Transport{
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: tr,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.Get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple helper class to avoid too much boilerplate in tests
|
||||||
|
type WebserverTest struct {
|
||||||
|
ServiceName string
|
||||||
|
Namespace string
|
||||||
|
Client *client.Client
|
||||||
|
|
||||||
|
TestId string
|
||||||
|
Labels map[string]string
|
||||||
|
|
||||||
|
pods map[string]bool
|
||||||
|
services map[string]bool
|
||||||
|
|
||||||
|
// Used for generating e.g. unique pod names
|
||||||
|
sequence int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebserverTest(client *client.Client, namespace string, serviceName string) *WebserverTest {
|
||||||
|
t := &WebserverTest{}
|
||||||
|
t.Client = client
|
||||||
|
t.Namespace = namespace
|
||||||
|
t.ServiceName = serviceName
|
||||||
|
t.TestId = t.ServiceName + "-" + string(util.NewUUID())
|
||||||
|
t.Labels = map[string]string{
|
||||||
|
"testid": t.TestId,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.pods = make(map[string]bool)
|
||||||
|
t.services = make(map[string]bool)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WebserverTest) SequenceNext() int {
|
||||||
|
n := atomic.AddInt32(&t.sequence, 1)
|
||||||
|
return int(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build default config for a service (which can then be changed)
|
||||||
|
func (t *WebserverTest) BuildServiceSpec() *api.Service {
|
||||||
|
service := &api.Service{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: t.ServiceName,
|
||||||
|
},
|
||||||
|
Spec: api.ServiceSpec{
|
||||||
|
Selector: t.Labels,
|
||||||
|
Ports: []api.ServicePort{{
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: util.NewIntOrStringFromInt(80),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a pod with the well-known webserver configuration, and record it for cleanup
|
||||||
|
func (t *WebserverTest) CreateWebserverPod() {
|
||||||
|
name := t.ServiceName + "-" + strconv.Itoa(t.SequenceNext())
|
||||||
|
pod := &api.Pod{
|
||||||
|
TypeMeta: api.TypeMeta{
|
||||||
|
Kind: "Pod",
|
||||||
|
APIVersion: latest.Version,
|
||||||
|
},
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Labels: t.Labels,
|
||||||
|
},
|
||||||
|
Spec: api.PodSpec{
|
||||||
|
Containers: []api.Container{
|
||||||
|
{
|
||||||
|
Name: "webserver",
|
||||||
|
Image: "gcr.io/google_containers/test-webserver",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, err := t.CreatePod(pod)
|
||||||
|
if err != nil {
|
||||||
|
Failf("Failed to create pod %s: %v", pod.Name, err)
|
||||||
|
}
|
||||||
|
expectNoError(waitForPodRunningInNamespace(t.Client, pod.Name, t.Namespace))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a pod, and record it for cleanup
|
||||||
|
func (t *WebserverTest) CreatePod(pod *api.Pod) (*api.Pod, error) {
|
||||||
|
podClient := t.Client.Pods(t.Namespace)
|
||||||
|
result, err := podClient.Create(pod)
|
||||||
|
if err == nil {
|
||||||
|
t.pods[pod.Name] = true
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a service, and record it for cleanup
|
||||||
|
func (t *WebserverTest) CreateService(service *api.Service) (*api.Service, error) {
|
||||||
|
result, err := t.Client.Services(t.Namespace).Create(service)
|
||||||
|
if err == nil {
|
||||||
|
t.services[service.Name] = true
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a service, and remove it from the cleanup list
|
||||||
|
func (t *WebserverTest) DeleteService(serviceName string) error {
|
||||||
|
err := t.Client.Services(t.Namespace).Delete(serviceName)
|
||||||
|
if err == nil {
|
||||||
|
delete(t.services, serviceName)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *WebserverTest) Cleanup() []error {
|
||||||
|
var errs []error
|
||||||
|
|
||||||
|
for podName := range t.pods {
|
||||||
|
podClient := t.Client.Pods(t.Namespace)
|
||||||
|
By("deleting pod " + podName + " in namespace " + t.Namespace)
|
||||||
|
err := podClient.Delete(podName, nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for serviceName := range t.services {
|
||||||
|
By("deleting service " + serviceName + " in namespace " + t.Namespace)
|
||||||
|
err := t.Client.Services(t.Namespace).Delete(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user