Merge pull request #12561 from ArtfulCoder/externalIPs

External IPs support
This commit is contained in:
Saad Ali 2015-08-20 17:23:49 -07:00
commit cd798a471f
20 changed files with 173 additions and 100 deletions

View File

@ -13430,12 +13430,12 @@
"type": "string", "type": "string",
"description": "type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP; see http://releases.k8s.io/HEAD/docs/user-guide/services.md#external-services" "description": "type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP; see http://releases.k8s.io/HEAD/docs/user-guide/services.md#external-services"
}, },
"deprecatedPublicIPs": { "externalIPs": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
}, },
"description": "deprecated. externally visible IPs (e.g. load balancers) that should be proxied to this service" "description": "externally visible IPs (e.g. load balancers) that should be proxied to this service"
}, },
"sessionAffinity": { "sessionAffinity": {
"type": "string", "type": "string",

View File

@ -749,6 +749,7 @@ _kubectl_expose()
flags+=("--container-port=") flags+=("--container-port=")
flags+=("--create-external-load-balancer") flags+=("--create-external-load-balancer")
flags+=("--dry-run") flags+=("--dry-run")
flags+=("--external-ip=")
flags+=("--filename=") flags+=("--filename=")
flags_with_completion+=("--filename") flags_with_completion+=("--filename")
flags_completion+=("__handle_filename_extension_flag json|yaml|yml") flags_completion+=("__handle_filename_extension_flag json|yaml|yml")
@ -768,7 +769,6 @@ _kubectl_expose()
flags+=("--overrides=") flags+=("--overrides=")
flags+=("--port=") flags+=("--port=")
flags+=("--protocol=") flags+=("--protocol=")
flags+=("--public-ip=")
flags+=("--selector=") flags+=("--selector=")
flags+=("--session-affinity=") flags+=("--session-affinity=")
flags+=("--show-all") flags+=("--show-all")

View File

@ -34,6 +34,10 @@ re\-use the labels from the resource it exposes.
\fB\-\-dry\-run\fP=false \fB\-\-dry\-run\fP=false
If true, only print the object that would be sent, without creating it. If true, only print the object that would be sent, without creating it.
.PP
\fB\-\-external\-ip\fP=""
External IP address to set for the service. The service can be accessed by this IP in addition to its generated service IP.
.PP .PP
\fB\-f\fP, \fB\-\-filename\fP=[] \fB\-f\fP, \fB\-\-filename\fP=[]
Filename, directory, or URL to a file identifying the resource to expose a service Filename, directory, or URL to a file identifying the resource to expose a service
@ -78,10 +82,6 @@ re\-use the labels from the resource it exposes.
\fB\-\-protocol\fP="TCP" \fB\-\-protocol\fP="TCP"
The network protocol for the service to be created. Default is 'tcp'. The network protocol for the service to be created. Default is 'tcp'.
.PP
\fB\-\-public\-ip\fP=""
Name of a public IP address to set for the service. The service will be assigned this IP in addition to its generated service IP.
.PP .PP
\fB\-\-selector\fP="" \fB\-\-selector\fP=""
A label selector to use for this service. If empty (the default) infer the selector from the replication controller. A label selector to use for this service. If empty (the default) infer the selector from the replication controller.

View File

@ -45,7 +45,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 (-f FILENAME | TYPE NAME) --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type] kubectl expose (-f FILENAME | TYPE NAME) --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [----external-ip=external-ip-of-service] [--type=type]
``` ```
### Examples ### Examples
@ -70,6 +70,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 (trumped by --type). 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.
--external-ip="": External IP address to set for the service. The service can be accessed by this IP in addition to its generated service IP.
-f, --filename=[]: Filename, directory, or URL to a file identifying the resource to expose a service -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to expose a service
--generator="service/v2": The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'. --generator="service/v2": The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'.
-h, --help[=false]: help for expose -h, --help[=false]: help for expose
@ -81,7 +82,6 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream
--overrides="": An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. --overrides="": An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.
--port=-1: The port that the service should serve on. Copied from the resource being exposed, if unspecified --port=-1: The port that the service should serve on. Copied from the resource being exposed, if unspecified
--protocol="TCP": The network protocol for the service to be created. Default is 'tcp'. --protocol="TCP": The network protocol for the service to be created. Default is 'tcp'.
--public-ip="": Name of a public IP address to set for the service. The service will be assigned this IP in addition to its generated service IP.
--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.
--session-affinity="": If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP' --session-affinity="": If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'
-a, --show-all[=false]: When printing, show all resources (default hide terminated pods.) -a, --show-all[=false]: When printing, show all resources (default hide terminated pods.)
@ -124,7 +124,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager
###### Auto generated by spf13/cobra at 2015-08-20 22:01:12.478645014 +0000 UTC ###### Auto generated by spf13/cobra at 2015-08-20 23:09:42.260392956 +0000 UTC
<!-- BEGIN MUNGE: GENERATED_ANALYTICS --> <!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_expose.md?pixel)]() [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_expose.md?pixel)]()

View File

@ -77,6 +77,7 @@ executor-suicide-timeout
experimental-keystone-url experimental-keystone-url
experimental-prefix experimental-prefix
external-hostname external-hostname
external-ip
failover-timeout failover-timeout
file-check-frequency file-check-frequency
file-suffix file-suffix
@ -180,7 +181,6 @@ proxy-bindall
proxy-logv proxy-logv
proxy-port-range proxy-port-range
public-address-override public-address-override
public-ip
pvclaimbinder-sync-period pvclaimbinder-sync-period
read-only-port read-only-port
really-crash-for-testing really-crash-for-testing

View File

@ -1949,13 +1949,13 @@ func deepCopy_api_ServiceSpec(in ServiceSpec, out *ServiceSpec, c *conversion.Cl
} }
out.ClusterIP = in.ClusterIP out.ClusterIP = in.ClusterIP
out.Type = in.Type out.Type = in.Type
if in.DeprecatedPublicIPs != nil { if in.ExternalIPs != nil {
out.DeprecatedPublicIPs = make([]string, len(in.DeprecatedPublicIPs)) out.ExternalIPs = make([]string, len(in.ExternalIPs))
for i := range in.DeprecatedPublicIPs { for i := range in.ExternalIPs {
out.DeprecatedPublicIPs[i] = in.DeprecatedPublicIPs[i] out.ExternalIPs[i] = in.ExternalIPs[i]
} }
} else { } else {
out.DeprecatedPublicIPs = nil out.ExternalIPs = nil
} }
out.SessionAffinity = in.SessionAffinity out.SessionAffinity = in.SessionAffinity
return nil return nil

View File

@ -1190,10 +1190,9 @@ type ServiceSpec struct {
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
Type ServiceType `json:"type,omitempty"` Type ServiceType `json:"type,omitempty"`
// DeprecatedPublicIPs are deprecated and silently ignored. // ExternalIPs are used by external load balancers, or can be set by
// 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.
DeprecatedPublicIPs []string `json:"deprecatedPublicIPs,omitempty"` ExternalIPs []string `json:"externalIPs,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"`

View File

@ -2166,13 +2166,13 @@ func convert_api_ServiceSpec_To_v1_ServiceSpec(in *api.ServiceSpec, out *Service
} }
out.ClusterIP = in.ClusterIP out.ClusterIP = in.ClusterIP
out.Type = ServiceType(in.Type) out.Type = ServiceType(in.Type)
if in.DeprecatedPublicIPs != nil { if in.ExternalIPs != nil {
out.DeprecatedPublicIPs = make([]string, len(in.DeprecatedPublicIPs)) out.ExternalIPs = make([]string, len(in.ExternalIPs))
for i := range in.DeprecatedPublicIPs { for i := range in.ExternalIPs {
out.DeprecatedPublicIPs[i] = in.DeprecatedPublicIPs[i] out.ExternalIPs[i] = in.ExternalIPs[i]
} }
} else { } else {
out.DeprecatedPublicIPs = nil out.ExternalIPs = nil
} }
out.SessionAffinity = ServiceAffinity(in.SessionAffinity) out.SessionAffinity = ServiceAffinity(in.SessionAffinity)
return nil return nil
@ -4581,13 +4581,13 @@ func convert_v1_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.Service
} }
out.ClusterIP = in.ClusterIP out.ClusterIP = in.ClusterIP
out.Type = api.ServiceType(in.Type) out.Type = api.ServiceType(in.Type)
if in.DeprecatedPublicIPs != nil { if in.ExternalIPs != nil {
out.DeprecatedPublicIPs = make([]string, len(in.DeprecatedPublicIPs)) out.ExternalIPs = make([]string, len(in.ExternalIPs))
for i := range in.DeprecatedPublicIPs { for i := range in.ExternalIPs {
out.DeprecatedPublicIPs[i] = in.DeprecatedPublicIPs[i] out.ExternalIPs[i] = in.ExternalIPs[i]
} }
} else { } else {
out.DeprecatedPublicIPs = nil out.ExternalIPs = nil
} }
out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity) out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity)
return nil return nil

View File

@ -1954,13 +1954,13 @@ func deepCopy_v1_ServiceSpec(in ServiceSpec, out *ServiceSpec, c *conversion.Clo
} }
out.ClusterIP = in.ClusterIP out.ClusterIP = in.ClusterIP
out.Type = in.Type out.Type = in.Type
if in.DeprecatedPublicIPs != nil { if in.ExternalIPs != nil {
out.DeprecatedPublicIPs = make([]string, len(in.DeprecatedPublicIPs)) out.ExternalIPs = make([]string, len(in.ExternalIPs))
for i := range in.DeprecatedPublicIPs { for i := range in.ExternalIPs {
out.DeprecatedPublicIPs[i] = in.DeprecatedPublicIPs[i] out.ExternalIPs[i] = in.ExternalIPs[i]
} }
} else { } else {
out.DeprecatedPublicIPs = nil out.ExternalIPs = nil
} }
out.SessionAffinity = in.SessionAffinity out.SessionAffinity = in.SessionAffinity
return nil return nil

View File

@ -1150,7 +1150,7 @@ type ServiceSpec struct {
// Deprecated. 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.
DeprecatedPublicIPs []string `json:"deprecatedPublicIPs,omitempty" description:"deprecated. externally visible IPs (e.g. load balancers) that should be proxied to this service"` ExternalIPs []string `json:"externalIPs,omitempty" description:"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; see http://releases.k8s.io/HEAD/docs/user-guide/services.md#virtual-ips-and-service-proxies"` SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty" description:"enable client IP based session affinity; must be ClientIP or None; defaults to None; see http://releases.k8s.io/HEAD/docs/user-guide/services.md#virtual-ips-and-service-proxies"`

View File

@ -1090,12 +1090,11 @@ func ValidateService(service *api.Service) errs.ValidationErrorList {
} }
} }
for _, ip := range service.Spec.DeprecatedPublicIPs { for _, ip := range service.Spec.ExternalIPs {
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.externalIPs", ip, "is not an IP address"))
} else if util.IsValidIPv4(ip) && net.ParseIP(ip).IsLoopback() {
allErrs = append(allErrs, errs.NewFieldInvalid("spec.publicIPs", ip, "publicIP cannot be a loopback"))
} }
allErrs = append(allErrs, validateIpIsNotLinkLocalOrLoopback(ip, "spec.externalIPs")...)
} }
if service.Spec.Type == "" { if service.Spec.Type == "" {
@ -1740,18 +1739,26 @@ func validateEndpointAddress(address *api.EndpointAddress) errs.ValidationErrorL
allErrs = append(allErrs, errs.NewFieldInvalid("ip", address.IP, "invalid IPv4 address")) allErrs = append(allErrs, errs.NewFieldInvalid("ip", address.IP, "invalid IPv4 address"))
return allErrs return allErrs
} }
// We disallow some IPs as endpoints. Specifically, loopback addresses are return validateIpIsNotLinkLocalOrLoopback(address.IP, "ip")
// nonsensical and link-local addresses tend to be used for node-centric }
// purposes (e.g. metadata service).
ip := net.ParseIP(address.IP) func validateIpIsNotLinkLocalOrLoopback(ipAddress, fieldName string) errs.ValidationErrorList {
// We disallow some IPs as endpoints or external-ips. Specifically, loopback addresses are
// nonsensical and link-local addresses tend to be used for node-centric purposes (e.g. metadata service).
allErrs := errs.ValidationErrorList{}
ip := net.ParseIP(ipAddress)
if ip == nil {
allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, ipAddress, "not a valid IP address"))
return allErrs
}
if ip.IsLoopback() { if ip.IsLoopback() {
allErrs = append(allErrs, errs.NewFieldInvalid("ip", address.IP, "may not be in the loopback range (127.0.0.0/8)")) allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, ipAddress, "may not be in the loopback range (127.0.0.0/8)"))
} }
if ip.IsLinkLocalUnicast() { if ip.IsLinkLocalUnicast() {
allErrs = append(allErrs, errs.NewFieldInvalid("ip", address.IP, "may not be in the link-local range (169.254.0.0/16)")) allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, ipAddress, "may not be in the link-local range (169.254.0.0/16)"))
} }
if ip.IsLinkLocalMulticast() { if ip.IsLinkLocalMulticast() {
allErrs = append(allErrs, errs.NewFieldInvalid("ip", address.IP, "may not be in the link-local multicast range (224.0.0.0/24)")) allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, ipAddress, "may not be in the link-local multicast range (224.0.0.0/24)"))
} }
return allErrs return allErrs
} }

View File

@ -1721,23 +1721,23 @@ 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.DeprecatedPublicIPs = []string{"127.0.0.1"} s.Spec.ExternalIPs = []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.DeprecatedPublicIPs = []string{"0.0.0.0"} s.Spec.ExternalIPs = []string{"0.0.0.0"}
}, },
numErrs: 1, numErrs: 1,
}, },
{ {
name: "valid publicIPs host", name: "invalid publicIPs host",
tweakSvc: func(s *api.Service) { tweakSvc: func(s *api.Service) {
s.Spec.DeprecatedPublicIPs = []string{"myhost.mydomain"} s.Spec.ExternalIPs = []string{"myhost.mydomain"}
}, },
numErrs: 0, numErrs: 1,
}, },
{ {
name: "dup port name", name: "dup port name",

View File

@ -378,8 +378,8 @@ func (s *ServiceController) createExternalLoadBalancer(service *api.Service) err
return err return err
} }
name := s.loadBalancerName(service) name := s.loadBalancerName(service)
if len(service.Spec.DeprecatedPublicIPs) > 0 { if len(service.Spec.ExternalIPs) > 0 {
for _, publicIP := range service.Spec.DeprecatedPublicIPs { for _, publicIP := range service.Spec.ExternalIPs {
// 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.
status, err := s.balancer.EnsureTCPLoadBalancer(name, s.zone.Region, net.ParseIP(publicIP), status, err := s.balancer.EnsureTCPLoadBalancer(name, s.zone.Region, net.ParseIP(publicIP),
@ -477,11 +477,11 @@ func needsUpdate(oldService *api.Service, newService *api.Service) bool {
if !portsEqualForLB(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity { if !portsEqualForLB(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity {
return true return true
} }
if len(oldService.Spec.DeprecatedPublicIPs) != len(newService.Spec.DeprecatedPublicIPs) { if len(oldService.Spec.ExternalIPs) != len(newService.Spec.ExternalIPs) {
return true return true
} }
for i := range oldService.Spec.DeprecatedPublicIPs { for i := range oldService.Spec.ExternalIPs {
if oldService.Spec.DeprecatedPublicIPs[i] != newService.Spec.DeprecatedPublicIPs[i] { if oldService.Spec.ExternalIPs[i] != newService.Spec.ExternalIPs[i] {
return true return true
} }
} }

View File

@ -49,7 +49,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 (-f FILENAME | TYPE NAME) --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type]", Use: "expose (-f FILENAME | TYPE NAME) --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [----external-ip=external-ip-of-service] [--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,
@ -70,7 +70,7 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command {
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.")
cmd.Flags().String("container-port", "", "Synonym for --target-port") cmd.Flags().String("container-port", "", "Synonym for --target-port")
cmd.Flags().String("target-port", "", "Name or number for the port on the container that the service should direct traffic to. Optional.") cmd.Flags().String("target-port", "", "Name or number for the port on the container that the service should direct traffic to. Optional.")
cmd.Flags().String("public-ip", "", "Name of a public IP address to set for the service. The service will be assigned this IP in addition to its generated service IP.") cmd.Flags().String("external-ip", "", "External IP address to set for the service. The service can be accessed by this IP in addition to its generated service IP.")
cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.") cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.")
cmd.Flags().String("name", "", "The name for the newly created object.") cmd.Flags().String("name", "", "The name for the newly created object.")
cmd.Flags().String("session-affinity", "", "If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'") cmd.Flags().String("session-affinity", "", "If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'")

View File

@ -585,8 +585,14 @@ func printReplicationControllerList(list *api.ReplicationControllerList, w io.Wr
func getServiceExternalIP(svc *api.Service) string { func getServiceExternalIP(svc *api.Service) string {
switch svc.Spec.Type { switch svc.Spec.Type {
case api.ServiceTypeClusterIP: case api.ServiceTypeClusterIP:
if len(svc.Spec.ExternalIPs) > 0 {
return strings.Join(svc.Spec.ExternalIPs, ",")
}
return "<none>" return "<none>"
case api.ServiceTypeNodePort: case api.ServiceTypeNodePort:
if len(svc.Spec.ExternalIPs) > 0 {
return strings.Join(svc.Spec.ExternalIPs, ",")
}
return "nodes" return "nodes"
case api.ServiceTypeLoadBalancer: case api.ServiceTypeLoadBalancer:
ingress := svc.Status.LoadBalancer.Ingress ingress := svc.Status.LoadBalancer.Ingress
@ -596,6 +602,9 @@ func getServiceExternalIP(svc *api.Service) string {
result = append(result, ingress[i].IP) result = append(result, ingress[i].IP)
} }
} }
if len(svc.Spec.ExternalIPs) > 0 {
result = append(result, svc.Spec.ExternalIPs...)
}
return strings.Join(result, ",") return strings.Join(result, ",")
} }
return "unknown" return "unknown"

View File

@ -18,11 +18,10 @@ package kubectl
import ( import (
"fmt" "fmt"
"strconv"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util"
"strconv"
) )
// The only difference between ServiceGeneratorV1 and V2 is that the service port is named "default" in V1, while it is left unnamed in V2. // The only difference between ServiceGeneratorV1 and V2 is that the service port is named "default" in V1, while it is left unnamed in V2.
@ -54,7 +53,7 @@ func paramNames() []GeneratorParam {
{"selector", true}, {"selector", true},
{"port", true}, {"port", true},
{"labels", false}, {"labels", false},
{"public-ip", false}, {"external-ip", false},
{"create-external-load-balancer", false}, {"create-external-load-balancer", false},
{"type", false}, {"type", false},
{"protocol", false}, {"protocol", false},
@ -144,8 +143,8 @@ func generate(genericParams map[string]interface{}) (runtime.Object, error) {
if params["create-external-load-balancer"] == "true" { if params["create-external-load-balancer"] == "true" {
service.Spec.Type = api.ServiceTypeLoadBalancer service.Spec.Type = api.ServiceTypeLoadBalancer
} }
if len(params["public-ip"]) != 0 { if len(params["external-ip"]) > 0 {
service.Spec.DeprecatedPublicIPs = []string{params["public-ip"]} service.Spec.ExternalIPs = []string{params["external-ip"]}
} }
if len(params["type"]) != 0 { if len(params["type"]) != 0 {
service.Spec.Type = api.ServiceType(params["type"]) service.Spec.Type = api.ServiceType(params["type"])

View File

@ -128,7 +128,7 @@ func TestGenerateService(t *testing.T) {
"port": "80", "port": "80",
"protocol": "UDP", "protocol": "UDP",
"container-port": "foobar", "container-port": "foobar",
"public-ip": "1.2.3.4", "external-ip": "1.2.3.4",
}, },
expected: api.Service{ expected: api.Service{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
@ -146,7 +146,7 @@ func TestGenerateService(t *testing.T) {
TargetPort: util.NewIntOrStringFromString("foobar"), TargetPort: util.NewIntOrStringFromString("foobar"),
}, },
}, },
DeprecatedPublicIPs: []string{"1.2.3.4"}, ExternalIPs: []string{"1.2.3.4"},
}, },
}, },
}, },
@ -158,7 +158,7 @@ func TestGenerateService(t *testing.T) {
"port": "80", "port": "80",
"protocol": "UDP", "protocol": "UDP",
"container-port": "foobar", "container-port": "foobar",
"public-ip": "1.2.3.4", "external-ip": "1.2.3.4",
"create-external-load-balancer": "true", "create-external-load-balancer": "true",
}, },
expected: api.Service{ expected: api.Service{
@ -178,7 +178,7 @@ func TestGenerateService(t *testing.T) {
}, },
}, },
Type: api.ServiceTypeLoadBalancer, Type: api.ServiceTypeLoadBalancer,
DeprecatedPublicIPs: []string{"1.2.3.4"}, ExternalIPs: []string{"1.2.3.4"},
}, },
}, },
}, },

View File

@ -130,7 +130,7 @@ type serviceInfo struct {
stickyMaxAgeSeconds int stickyMaxAgeSeconds int
endpoints []string endpoints []string
// Deprecated, but required for back-compat (including e2e) // Deprecated, but required for back-compat (including e2e)
deprecatedPublicIPs []string externalIPs []string
} }
// returns a new serviceInfo struct // returns a new serviceInfo struct
@ -236,7 +236,7 @@ func (proxier *Proxier) sameConfig(info *serviceInfo, service *api.Service, port
if !info.clusterIP.Equal(net.ParseIP(service.Spec.ClusterIP)) { if !info.clusterIP.Equal(net.ParseIP(service.Spec.ClusterIP)) {
return false return false
} }
if !ipsEqual(info.deprecatedPublicIPs, service.Spec.DeprecatedPublicIPs) { if !ipsEqual(info.externalIPs, service.Spec.ExternalIPs) {
return false return false
} }
if !api.LoadBalancerStatusEqual(&info.loadBalancerStatus, &service.Status.LoadBalancer) { if !api.LoadBalancerStatusEqual(&info.loadBalancerStatus, &service.Status.LoadBalancer) {
@ -318,7 +318,7 @@ func (proxier *Proxier) OnServiceUpdate(allServices []api.Service) {
info.port = servicePort.Port info.port = servicePort.Port
info.protocol = servicePort.Protocol info.protocol = servicePort.Protocol
info.nodePort = servicePort.NodePort info.nodePort = servicePort.NodePort
info.deprecatedPublicIPs = service.Spec.DeprecatedPublicIPs info.externalIPs = service.Spec.ExternalIPs
// Deep-copy in case the service instance changes // Deep-copy in case the service instance changes
info.loadBalancerStatus = *api.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer) info.loadBalancerStatus = *api.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer)
info.sessionAffinityType = service.Spec.SessionAffinity info.sessionAffinityType = service.Spec.SessionAffinity
@ -556,7 +556,7 @@ func (proxier *Proxier) syncProxyRules() error {
"-j", string(svcChain)) "-j", string(svcChain))
// Capture externalIPs. // Capture externalIPs.
for _, externalIP := range info.deprecatedPublicIPs { for _, externalIP := range info.externalIPs {
args := []string{ args := []string{
"-A", string(iptablesServicesChain), "-A", string(iptablesServicesChain),
"-m", "comment", "--comment", fmt.Sprintf("\"%s external IP\"", name.String()), "-m", "comment", "--comment", fmt.Sprintf("\"%s external IP\"", name.String()),
@ -564,10 +564,23 @@ func (proxier *Proxier) syncProxyRules() error {
"-d", fmt.Sprintf("%s/32", externalIP), "-d", fmt.Sprintf("%s/32", externalIP),
"--dport", fmt.Sprintf("%d", info.port), "--dport", fmt.Sprintf("%d", info.port),
} }
// We have to SNAT packets from external IPs. // We have to SNAT packets to external IPs.
writeLine(rulesLines, append(args, writeLine(rulesLines, append(args,
"-j", "MARK", "--set-xmark", fmt.Sprintf("%s/0xffffffff", iptablesMasqueradeMark))...) "-j", "MARK", "--set-xmark", fmt.Sprintf("%s/0xffffffff", iptablesMasqueradeMark))...)
writeLine(rulesLines, append(args,
// Allow traffic for external IPs that does not come from a bridge (i.e. not from a container)
// nor from a local process to be forwarded to the service.
// This rule roughly translates to "all traffic from off-machine".
// This is imperfect in the face of network plugins that might not use a bridge, but we can revisit that later.
externalTrafficOnlyArgs := append(args,
"-m", "physdev", "!", "--physdev-is-in",
"-m", "addrtype", "!", "--src-type", "LOCAL")
writeLine(rulesLines, append(externalTrafficOnlyArgs,
"-j", string(svcChain))...)
dstLocalOnlyArgs := append(args, "-m", "addrtype", "--dst-type", "LOCAL")
// Allow traffic bound for external IPs that happen to be recognized as local IPs to stay local.
// This covers cases like GCE load-balancers which get added to the local routing table.
writeLine(rulesLines, append(dstLocalOnlyArgs,
"-j", string(svcChain))...) "-j", string(svcChain))...)
} }

View File

@ -37,6 +37,7 @@ import (
type portal struct { type portal struct {
ip net.IP ip net.IP
port int port int
isExternal bool
} }
type serviceInfo struct { type serviceInfo struct {
@ -51,7 +52,7 @@ type serviceInfo struct {
sessionAffinityType api.ServiceAffinity sessionAffinityType api.ServiceAffinity
stickyMaxAgeMinutes int stickyMaxAgeMinutes int
// Deprecated, but required for back-compat (including e2e) // Deprecated, but required for back-compat (including e2e)
deprecatedPublicIPs []string externalIPs []string
} }
func logTimeout(err error) bool { func logTimeout(err error) bool {
@ -330,7 +331,7 @@ func (proxier *Proxier) OnServiceUpdate(services []api.Service) {
} }
info.portal.ip = serviceIP info.portal.ip = serviceIP
info.portal.port = servicePort.Port info.portal.port = servicePort.Port
info.deprecatedPublicIPs = service.Spec.DeprecatedPublicIPs info.externalIPs = service.Spec.ExternalIPs
// Deep-copy in case the service instance changes // Deep-copy in case the service instance changes
info.loadBalancerStatus = *api.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer) info.loadBalancerStatus = *api.LoadBalancerStatusDeepCopy(&service.Status.LoadBalancer)
info.nodePort = servicePort.NodePort info.nodePort = servicePort.NodePort
@ -368,7 +369,7 @@ func sameConfig(info *serviceInfo, service *api.Service, port *api.ServicePort)
if !info.portal.ip.Equal(net.ParseIP(service.Spec.ClusterIP)) { if !info.portal.ip.Equal(net.ParseIP(service.Spec.ClusterIP)) {
return false return false
} }
if !ipsEqual(info.deprecatedPublicIPs, service.Spec.DeprecatedPublicIPs) { if !ipsEqual(info.externalIPs, service.Spec.ExternalIPs) {
return false return false
} }
if !api.LoadBalancerStatusEqual(&info.loadBalancerStatus, &service.Status.LoadBalancer) { if !api.LoadBalancerStatusEqual(&info.loadBalancerStatus, &service.Status.LoadBalancer) {
@ -397,15 +398,15 @@ func (proxier *Proxier) openPortal(service proxy.ServicePortName, info *serviceI
if err != nil { if err != nil {
return err return err
} }
for _, publicIP := range info.deprecatedPublicIPs { for _, publicIP := range info.externalIPs {
err = proxier.openOnePortal(portal{net.ParseIP(publicIP), info.portal.port}, info.protocol, proxier.listenIP, info.proxyPort, service) err = proxier.openOnePortal(portal{net.ParseIP(publicIP), info.portal.port, true}, info.protocol, proxier.listenIP, info.proxyPort, service)
if err != nil { if err != nil {
return err return err
} }
} }
for _, ingress := range info.loadBalancerStatus.Ingress { for _, ingress := range info.loadBalancerStatus.Ingress {
if ingress.IP != "" { if ingress.IP != "" {
err = proxier.openOnePortal(portal{net.ParseIP(ingress.IP), info.portal.port}, info.protocol, proxier.listenIP, info.proxyPort, service) err = proxier.openOnePortal(portal{net.ParseIP(ingress.IP), info.portal.port, false}, info.protocol, proxier.listenIP, info.proxyPort, service)
if err != nil { if err != nil {
return err return err
} }
@ -422,18 +423,40 @@ func (proxier *Proxier) openPortal(service proxy.ServicePortName, info *serviceI
func (proxier *Proxier) openOnePortal(portal portal, protocol api.Protocol, proxyIP net.IP, proxyPort int, name proxy.ServicePortName) error { func (proxier *Proxier) openOnePortal(portal portal, protocol api.Protocol, proxyIP net.IP, proxyPort int, name proxy.ServicePortName) error {
// Handle traffic from containers. // Handle traffic from containers.
args := proxier.iptablesContainerPortalArgs(portal.ip, portal.port, protocol, proxyIP, proxyPort, name) args := proxier.iptablesContainerPortalArgs(portal.ip, portal.isExternal, false, portal.port, protocol, proxyIP, proxyPort, name)
existed, err := proxier.iptables.EnsureRule(iptables.Append, 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, args:%v", iptablesContainerPortalChain, name, args)
return err return err
} }
if !existed { if !existed {
glog.V(3).Infof("Opened iptables from-containers portal for service %q on %s %s:%d", name, protocol, portal.ip, portal.port) glog.V(3).Infof("Opened iptables from-containers portal for service %q on %s %s:%d", name, protocol, portal.ip, portal.port)
} }
if portal.isExternal {
args := proxier.iptablesContainerPortalArgs(portal.ip, false, true, portal.port, protocol, proxyIP, proxyPort, name)
existed, err := proxier.iptables.EnsureRule(iptables.Append, iptables.TableNAT, iptablesContainerPortalChain, args...)
if err != nil {
glog.Errorf("Failed to install iptables %s rule that opens service %q for local traffic, args:%v", iptablesContainerPortalChain, name, args)
return err
}
if !existed {
glog.V(3).Infof("Opened iptables from-containers portal for service %q on %s %s:%d for local traffic", name, protocol, portal.ip, portal.port)
}
args = proxier.iptablesHostPortalArgs(portal.ip, true, portal.port, protocol, proxyIP, proxyPort, name)
existed, err = proxier.iptables.EnsureRule(iptables.Append, iptables.TableNAT, iptablesHostPortalChain, args...)
if err != nil {
glog.Errorf("Failed to install iptables %s rule for service %q for dst-local traffic", iptablesHostPortalChain, name)
return err
}
if !existed {
glog.V(3).Infof("Opened iptables from-host portal for service %q on %s %s:%d for dst-local traffic", name, protocol, portal.ip, portal.port)
}
return nil
}
// Handle traffic from the host. // Handle traffic from the host.
args = proxier.iptablesHostPortalArgs(portal.ip, portal.port, protocol, proxyIP, proxyPort, name) args = proxier.iptablesHostPortalArgs(portal.ip, false, portal.port, protocol, proxyIP, proxyPort, name)
existed, err = proxier.iptables.EnsureRule(iptables.Append, 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)
@ -522,12 +545,12 @@ func (proxier *Proxier) openNodePort(nodePort int, protocol api.Protocol, proxyI
func (proxier *Proxier) closePortal(service proxy.ServicePortName, info *serviceInfo) error { func (proxier *Proxier) closePortal(service proxy.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.portal, info.protocol, proxier.listenIP, info.proxyPort, service) el := proxier.closeOnePortal(info.portal, info.protocol, proxier.listenIP, info.proxyPort, service)
for _, publicIP := range info.deprecatedPublicIPs { for _, publicIP := range info.externalIPs {
el = append(el, proxier.closeOnePortal(portal{net.ParseIP(publicIP), info.portal.port}, info.protocol, proxier.listenIP, info.proxyPort, service)...) el = append(el, proxier.closeOnePortal(portal{net.ParseIP(publicIP), info.portal.port, true}, info.protocol, proxier.listenIP, info.proxyPort, service)...)
} }
for _, ingress := range info.loadBalancerStatus.Ingress { for _, ingress := range info.loadBalancerStatus.Ingress {
if ingress.IP != "" { if ingress.IP != "" {
el = append(el, proxier.closeOnePortal(portal{net.ParseIP(ingress.IP), info.portal.port}, info.protocol, proxier.listenIP, info.proxyPort, service)...) el = append(el, proxier.closeOnePortal(portal{net.ParseIP(ingress.IP), info.portal.port, false}, info.protocol, proxier.listenIP, info.proxyPort, service)...)
} }
} }
if info.nodePort != 0 { if info.nodePort != 0 {
@ -545,14 +568,29 @@ func (proxier *Proxier) closeOnePortal(portal portal, protocol api.Protocol, pro
el := []error{} el := []error{}
// Handle traffic from containers. // Handle traffic from containers.
args := proxier.iptablesContainerPortalArgs(portal.ip, portal.port, protocol, proxyIP, proxyPort, name) args := proxier.iptablesContainerPortalArgs(portal.ip, portal.isExternal, false, portal.port, protocol, proxyIP, proxyPort, name)
if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesContainerPortalChain, args...); err != nil { if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesContainerPortalChain, args...); err != nil {
glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesContainerPortalChain, name) glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesContainerPortalChain, name)
el = append(el, err) el = append(el, err)
} }
// Handle traffic from the host. if portal.isExternal {
args = proxier.iptablesHostPortalArgs(portal.ip, portal.port, protocol, proxyIP, proxyPort, name) args := proxier.iptablesContainerPortalArgs(portal.ip, false, true, portal.port, protocol, proxyIP, proxyPort, name)
if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesContainerPortalChain, args...); err != nil {
glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesContainerPortalChain, name)
el = append(el, err)
}
args = proxier.iptablesHostPortalArgs(portal.ip, true, portal.port, protocol, proxyIP, proxyPort, name)
if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesHostPortalChain, args...); err != nil {
glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesHostPortalChain, name)
el = append(el, err)
}
return el
}
// Handle traffic from the host (portalIP is not external).
args = proxier.iptablesHostPortalArgs(portal.ip, false, portal.port, protocol, proxyIP, proxyPort, name)
if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesHostPortalChain, args...); err != nil { if err := proxier.iptables.DeleteRule(iptables.TableNAT, iptablesHostPortalChain, args...); err != nil {
glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesHostPortalChain, name) glog.Errorf("Failed to delete iptables %s rule for service %q", iptablesHostPortalChain, name)
el = append(el, err) el = append(el, err)
@ -681,7 +719,7 @@ var zeroIPv6 = net.ParseIP("::0")
var localhostIPv6 = net.ParseIP("::1") var localhostIPv6 = net.ParseIP("::1")
// Build a slice of iptables args that are common to from-container and from-host portal rules. // Build a slice of iptables args that are common to from-container and from-host portal rules.
func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, service proxy.ServicePortName) []string { func iptablesCommonPortalArgs(destIP net.IP, addPhysicalInterfaceMatch bool, addDstLocalMatch bool, destPort int, protocol api.Protocol, service proxy.ServicePortName) []string {
// This list needs to include all fields as they are eventually spit out // This list needs to include all fields as they are eventually spit out
// by iptables-save. This is because some systems do not support the // by iptables-save. This is because some systems do not support the
// 'iptables -C' arg, and so fall back on parsing iptables-save output. // 'iptables -C' arg, and so fall back on parsing iptables-save output.
@ -702,12 +740,20 @@ func iptablesCommonPortalArgs(destIP net.IP, destPort int, protocol api.Protocol
args = append(args, "-d", fmt.Sprintf("%s/32", destIP.String())) args = append(args, "-d", fmt.Sprintf("%s/32", destIP.String()))
} }
if addPhysicalInterfaceMatch {
args = append(args, "-m", "physdev", "!", "--physdev-is-in")
}
if addDstLocalMatch {
args = append(args, "-m", "addrtype", "--dst-type", "LOCAL")
}
return args return args
} }
// Build a slice of iptables args for a from-container portal rule. // Build a slice of iptables args for a from-container portal rule.
func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string { func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, addPhysicalInterfaceMatch bool, addDstLocalMatch bool, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string {
args := iptablesCommonPortalArgs(destIP, destPort, protocol, service) args := iptablesCommonPortalArgs(destIP, addPhysicalInterfaceMatch, addDstLocalMatch, destPort, protocol, service)
// This is tricky. // This is tricky.
// //
@ -753,8 +799,8 @@ func (proxier *Proxier) iptablesContainerPortalArgs(destIP net.IP, destPort int,
} }
// Build a slice of iptables args for a from-host portal rule. // Build a slice of iptables args for a from-host portal rule.
func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string { func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, addDstLocalMatch bool, destPort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string {
args := iptablesCommonPortalArgs(destIP, destPort, protocol, service) args := iptablesCommonPortalArgs(destIP, false, addDstLocalMatch, destPort, protocol, service)
// This is tricky. // This is tricky.
// //
@ -789,7 +835,7 @@ func (proxier *Proxier) iptablesHostPortalArgs(destIP net.IP, destPort int, prot
// See iptablesContainerPortalArgs // See iptablesContainerPortalArgs
// TODO: Should we just reuse iptablesContainerPortalArgs? // TODO: Should we just reuse iptablesContainerPortalArgs?
func (proxier *Proxier) iptablesContainerNodePortArgs(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string { func (proxier *Proxier) iptablesContainerNodePortArgs(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string {
args := iptablesCommonPortalArgs(nil, nodePort, protocol, service) args := iptablesCommonPortalArgs(nil, false, false, nodePort, protocol, service)
if proxyIP.Equal(zeroIPv4) || proxyIP.Equal(zeroIPv6) { if proxyIP.Equal(zeroIPv4) || proxyIP.Equal(zeroIPv6) {
// TODO: Can we REDIRECT with IPv6? // TODO: Can we REDIRECT with IPv6?
@ -806,7 +852,7 @@ func (proxier *Proxier) iptablesContainerNodePortArgs(nodePort int, protocol api
// See iptablesHostPortalArgs // See iptablesHostPortalArgs
// TODO: Should we just reuse iptablesHostPortalArgs? // TODO: Should we just reuse iptablesHostPortalArgs?
func (proxier *Proxier) iptablesHostNodePortArgs(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string { func (proxier *Proxier) iptablesHostNodePortArgs(nodePort int, protocol api.Protocol, proxyIP net.IP, proxyPort int, service proxy.ServicePortName) []string {
args := iptablesCommonPortalArgs(nil, nodePort, protocol, service) args := iptablesCommonPortalArgs(nil, false, false, nodePort, protocol, service)
if proxyIP.Equal(zeroIPv4) || proxyIP.Equal(zeroIPv6) { if proxyIP.Equal(zeroIPv4) || proxyIP.Equal(zeroIPv6) {
proxyIP = proxier.hostIP proxyIP = proxier.hostIP

View File

@ -788,7 +788,7 @@ func TestProxyUpdatePublicIPs(t *testing.T) {
Protocol: "TCP", Protocol: "TCP",
}}, }},
ClusterIP: svcInfo.portal.ip.String(), ClusterIP: svcInfo.portal.ip.String(),
DeprecatedPublicIPs: []string{"4.3.2.1"}, ExternalIPs: []string{"4.3.2.1"},
}, },
}}) }})
// Wait for the socket to actually get free. // Wait for the socket to actually get free.