From 44ce4aa423fca40dc392c625b83d1ccc635c4248 Mon Sep 17 00:00:00 2001 From: Abhishek Shah Date: Thu, 20 Aug 2015 18:23:24 -0700 Subject: [PATCH] Create a LB for a K8S with the LB-IP provided by user. --- api/swagger-spec/v1.json | 4 ++ contrib/completions/bash/kubectl | 1 + docs/man/man1/kubectl-expose.1 | 4 ++ docs/user-guide/kubectl/kubectl_expose.md | 1 + docs/user-guide/services.md | 7 +- hack/verify-flags/known-flags.txt | 1 + pkg/api/deep_copy_generated.go | 3 +- pkg/api/types.go | 13 +++- pkg/api/v1/conversion_generated.go | 4 +- pkg/api/v1/deep_copy_generated.go | 1 + pkg/api/v1/types.go | 7 ++ pkg/api/v1/types_swagger_doc_generated.go | 1 + pkg/cloudprovider/cloud.go | 2 +- pkg/cloudprovider/providers/gce/gce.go | 6 +- .../providers/openstack/openstack.go | 8 +-- pkg/controller/service/servicecontroller.go | 33 ++++----- pkg/kubectl/cmd/expose.go | 1 + pkg/kubectl/service.go | 4 ++ test/e2e/google_compute.go | 67 ++++++++++++++++++ test/e2e/service.go | 70 +++++++++++++++++++ 20 files changed, 206 insertions(+), 32 deletions(-) create mode 100644 test/e2e/google_compute.go diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 2a74f5eda0c..b73388c045b 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -13662,6 +13662,10 @@ "sessionAffinity": { "type": "string", "description": "Supports \"ClientIP\" and \"None\". Used to maintain session affinity. Enable client IP based session affinity. Must be ClientIP or None. Defaults to None. More info: http://releases.k8s.io/HEAD/docs/user-guide/services.md#virtual-ips-and-service-proxies" + }, + "loadBalancerIP": { + "type": "string", + "description": "Only applies to Service Type: LoadBalancer LoadBalancer will get created with the IP specified in this field. This feature depends on whether the underlying cloud-provider supports specifying the loadBalancerIP when a load balancer is created. This field will be ignored if the cloud-provider does not support the feature." } } }, diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 0413c2a5260..025b4c7afb1 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -762,6 +762,7 @@ _kubectl_expose() flags+=("--generator=") flags+=("--labels=") two_word_flags+=("-l") + flags+=("--load-balancer-ip=") flags+=("--name=") flags+=("--no-headers") flags+=("--output=") diff --git a/docs/man/man1/kubectl-expose.1 b/docs/man/man1/kubectl-expose.1 index 513310db4f2..18a2d871095 100644 --- a/docs/man/man1/kubectl-expose.1 +++ b/docs/man/man1/kubectl-expose.1 @@ -50,6 +50,10 @@ re\-use the labels from the resource it exposes. \fB\-l\fP, \fB\-\-labels\fP="" Labels to apply to the service created by this call. +.PP +\fB\-\-load\-balancer\-ip\fP="" + IP to assign to to the Load Balancer. If empty, an ephemeral IP will be created and used(cloud\-provider specific). + .PP \fB\-\-name\fP="" The name for the newly created object. diff --git a/docs/user-guide/kubectl/kubectl_expose.md b/docs/user-guide/kubectl/kubectl_expose.md index 5d1e82a957c..c8ed770ffbc 100644 --- a/docs/user-guide/kubectl/kubectl_expose.md +++ b/docs/user-guide/kubectl/kubectl_expose.md @@ -73,6 +73,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream -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'. -l, --labels="": Labels to apply to the service created by this call. + --load-balancer-ip="": IP to assign to to the Load Balancer. If empty, an ephemeral IP will be created and used(cloud-provider specific). --name="": The name for the newly created object. --no-headers[=false]: When using the default output, don't print headers. -o, --output="": Output format. One of: json|yaml|wide|name|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://releases.k8s.io/HEAD/docs/user-guide/jsonpath.md]. diff --git a/docs/user-guide/services.md b/docs/user-guide/services.md index 53a328e3c47..f271f177711 100644 --- a/docs/user-guide/services.md +++ b/docs/user-guide/services.md @@ -433,6 +433,7 @@ information about the provisioned balancer will be published in the `Service`'s } ], "clusterIP": "10.0.171.239", + "loadBalancerIP": "78.11.24.19", "type": "LoadBalancer" }, "status": { @@ -448,7 +449,11 @@ information about the provisioned balancer will be published in the `Service`'s ``` Traffic from the external load balancer will be directed at the backend `Pods`, -though exactly how that works depends on the cloud provider. +though exactly how that works depends on the cloud provider. Some cloud providers allow +the `loadBalancerIP` to be specified. In those cases, the load-balancer will be created +with the user-specified `loadBalancerIP`. If the `loadBalancerIP` field is not specified, +an ephemeral IP will be assigned to the loadBalancer. If the `loadBalancerIP` is specified, but the +cloud provider does not support the feature, the field will be ignored. ## Shortcomings diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index c082aa7a767..1aba1fc83b9 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -138,6 +138,7 @@ kube-master label-columns last-release-pr legacy-userspace-proxy +load-balancer-ip log-flush-frequency long-running-request-regexp low-diskspace-threshold-mb diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index b7d8f958a7a..8771497cce1 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -1958,6 +1958,7 @@ func deepCopy_api_ServicePort(in ServicePort, out *ServicePort, c *conversion.Cl } func deepCopy_api_ServiceSpec(in ServiceSpec, out *ServiceSpec, c *conversion.Cloner) error { + out.Type = in.Type if in.Ports != nil { out.Ports = make([]ServicePort, len(in.Ports)) for i := range in.Ports { @@ -1977,7 +1978,6 @@ func deepCopy_api_ServiceSpec(in ServiceSpec, out *ServiceSpec, c *conversion.Cl out.Selector = nil } out.ClusterIP = in.ClusterIP - out.Type = in.Type if in.ExternalIPs != nil { out.ExternalIPs = make([]string, len(in.ExternalIPs)) for i := range in.ExternalIPs { @@ -1986,6 +1986,7 @@ func deepCopy_api_ServiceSpec(in ServiceSpec, out *ServiceSpec, c *conversion.Cl } else { out.ExternalIPs = nil } + out.LoadBalancerIP = in.LoadBalancerIP out.SessionAffinity = in.SessionAffinity return nil } diff --git a/pkg/api/types.go b/pkg/api/types.go index cd36da099c3..a3c89b2e45e 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1185,6 +1185,9 @@ type LoadBalancerIngress struct { // ServiceSpec describes the attributes that a user creates on a service type ServiceSpec struct { + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty"` + // Required: The list of ports that are exposed by this service. Ports []ServicePort `json:"ports"` @@ -1200,13 +1203,17 @@ type ServiceSpec struct { // None can be specified for headless services when proxying is not required ClusterIP string `json:"clusterIP,omitempty"` - // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer - Type ServiceType `json:"type,omitempty"` - // ExternalIPs are used by external load balancers, or can be set by // users to handle external traffic that arrives at a node. ExternalIPs []string `json:"externalIPs,omitempty"` + // Only applies to Service Type: LoadBalancer + // LoadBalancer will get created with the IP specified in this field. + // This feature depends on whether the underlying cloud-provider supports specifying + // the loadBalancerIP when a load balancer is created. + // This field will be ignored if the cloud-provider does not support the feature. + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + // Required: Supports "ClientIP" and "None". Used to maintain session affinity. SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty"` } diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 70a047e8695..2e1c997facd 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -2172,6 +2172,7 @@ func convert_api_ServiceSpec_To_v1_ServiceSpec(in *api.ServiceSpec, out *Service if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.ServiceSpec))(in) } + out.Type = ServiceType(in.Type) if in.Ports != nil { out.Ports = make([]ServicePort, len(in.Ports)) for i := range in.Ports { @@ -2191,7 +2192,6 @@ func convert_api_ServiceSpec_To_v1_ServiceSpec(in *api.ServiceSpec, out *Service out.Selector = nil } out.ClusterIP = in.ClusterIP - out.Type = ServiceType(in.Type) if in.ExternalIPs != nil { out.ExternalIPs = make([]string, len(in.ExternalIPs)) for i := range in.ExternalIPs { @@ -2200,6 +2200,7 @@ func convert_api_ServiceSpec_To_v1_ServiceSpec(in *api.ServiceSpec, out *Service } else { out.ExternalIPs = nil } + out.LoadBalancerIP = in.LoadBalancerIP out.SessionAffinity = ServiceAffinity(in.SessionAffinity) return nil } @@ -4603,6 +4604,7 @@ func convert_v1_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.Service out.ExternalIPs = nil } out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity) + out.LoadBalancerIP = in.LoadBalancerIP return nil } diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index e5db68e2478..d5e2247a67f 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -1992,6 +1992,7 @@ func deepCopy_v1_ServiceSpec(in ServiceSpec, out *ServiceSpec, c *conversion.Clo out.ExternalIPs = nil } out.SessionAffinity = in.SessionAffinity + out.LoadBalancerIP = in.LoadBalancerIP return nil } diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index d70760b9e2a..6d0f3241932 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1509,6 +1509,13 @@ type ServiceSpec struct { // Defaults to None. // More info: http://releases.k8s.io/HEAD/docs/user-guide/services.md#virtual-ips-and-service-proxies SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty"` + + // Only applies to Service Type: LoadBalancer + // LoadBalancer will get created with the IP specified in this field. + // This feature depends on whether the underlying cloud-provider supports specifying + // the loadBalancerIP when a load balancer is created. + // This field will be ignored if the cloud-provider does not support the feature. + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` } // ServicePort conatins information on service's port. diff --git a/pkg/api/v1/types_swagger_doc_generated.go b/pkg/api/v1/types_swagger_doc_generated.go index ef56378a642..c8fa1ab436d 100644 --- a/pkg/api/v1/types_swagger_doc_generated.go +++ b/pkg/api/v1/types_swagger_doc_generated.go @@ -1272,6 +1272,7 @@ var map_ServiceSpec = map[string]string{ "type": "Type of exposed service. Must be ClusterIP, NodePort, or LoadBalancer. Defaults to ClusterIP. More info: http://releases.k8s.io/HEAD/docs/user-guide/services.md#external-services", "externalIPs": "ExternalIPs are used by external load balancers, or can be set by users to handle external traffic that arrives at a node. Externally visible IPs (e.g. load balancers) that should be proxied to this service.", "sessionAffinity": "Supports \"ClientIP\" and \"None\". Used to maintain session affinity. Enable client IP based session affinity. Must be ClientIP or None. Defaults to None. More info: http://releases.k8s.io/HEAD/docs/user-guide/services.md#virtual-ips-and-service-proxies", + "loadBalancerIP": "Only applies to Service Type: LoadBalancer LoadBalancer will get created with the IP specified in this field. This feature depends on whether the underlying cloud-provider supports specifying the loadBalancerIP when a load balancer is created. This field will be ignored if the cloud-provider does not support the feature.", } func (ServiceSpec) SwaggerDoc() map[string]string { diff --git a/pkg/cloudprovider/cloud.go b/pkg/cloudprovider/cloud.go index 50241fd28de..9212f740a6c 100644 --- a/pkg/cloudprovider/cloud.go +++ b/pkg/cloudprovider/cloud.go @@ -81,7 +81,7 @@ type TCPLoadBalancer interface { // if so, what its status is. GetTCPLoadBalancer(name, region string) (status *api.LoadBalancerStatus, exists bool, err error) // EnsureTCPLoadBalancer creates a new tcp load balancer, or updates an existing one. Returns the status of the balancer - EnsureTCPLoadBalancer(name, region string, externalIP net.IP, ports []*api.ServicePort, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, error) + EnsureTCPLoadBalancer(name, region string, loadBalancerIP net.IP, ports []*api.ServicePort, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, error) // UpdateTCPLoadBalancer updates hosts under the specified load balancer. UpdateTCPLoadBalancer(name, region string, hosts []string) error // EnsureTCPLoadBalancerDeleted deletes the specified load balancer if it diff --git a/pkg/cloudprovider/providers/gce/gce.go b/pkg/cloudprovider/providers/gce/gce.go index 1926b2683ba..c6987de8030 100644 --- a/pkg/cloudprovider/providers/gce/gce.go +++ b/pkg/cloudprovider/providers/gce/gce.go @@ -352,7 +352,7 @@ func makeFirewallName(name string) string { // EnsureTCPLoadBalancer is an implementation of TCPLoadBalancer.EnsureTCPLoadBalancer. // 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. -func (gce *GCECloud) EnsureTCPLoadBalancer(name, region string, externalIP net.IP, ports []*api.ServicePort, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, error) { +func (gce *GCECloud) EnsureTCPLoadBalancer(name, region string, loadBalancerIP net.IP, ports []*api.ServicePort, hosts []string, affinityType api.ServiceAffinity) (*api.LoadBalancerStatus, error) { if len(hosts) == 0 { return nil, fmt.Errorf("Cannot EnsureTCPLoadBalancer() with no hosts") } @@ -399,6 +399,10 @@ func (gce *GCECloud) EnsureTCPLoadBalancer(name, region string, externalIP net.I PortRange: fmt.Sprintf("%d-%d", minPort, maxPort), Target: gce.targetPoolURL(name, region), } + if loadBalancerIP != nil { + req.IPAddress = loadBalancerIP.String() + } + op, err := gce.service.ForwardingRules.Insert(gce.projectID, region, req).Do() if err != nil && !isHTTPErrorCode(err, http.StatusConflict) { return nil, err diff --git a/pkg/cloudprovider/providers/openstack/openstack.go b/pkg/cloudprovider/providers/openstack/openstack.go index 8bf1ef69d90..f5f814b8321 100644 --- a/pkg/cloudprovider/providers/openstack/openstack.go +++ b/pkg/cloudprovider/providers/openstack/openstack.go @@ -525,8 +525,8 @@ func (lb *LoadBalancer) GetTCPLoadBalancer(name, region string) (*api.LoadBalanc // a list of regions (from config) and query/create loadbalancers in // each region. -func (lb *LoadBalancer) EnsureTCPLoadBalancer(name, region string, externalIP net.IP, ports []*api.ServicePort, hosts []string, affinity api.ServiceAffinity) (*api.LoadBalancerStatus, error) { - glog.V(4).Infof("EnsureTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, externalIP, ports, hosts, affinity) +func (lb *LoadBalancer) EnsureTCPLoadBalancer(name, region string, loadBalancerIP net.IP, ports []*api.ServicePort, hosts []string, affinity api.ServiceAffinity) (*api.LoadBalancerStatus, error) { + glog.V(4).Infof("EnsureTCPLoadBalancer(%v, %v, %v, %v, %v, %v)", name, region, loadBalancerIP, ports, hosts, affinity) if len(ports) > 1 { return nil, fmt.Errorf("multiple ports are not yet supported in openstack load balancers") @@ -618,8 +618,8 @@ func (lb *LoadBalancer) EnsureTCPLoadBalancer(name, region string, externalIP ne SubnetID: lb.opts.SubnetId, Persistence: persistence, } - if externalIP != nil { - createOpts.Address = externalIP.String() + if loadBalancerIP != nil { + createOpts.Address = loadBalancerIP.String() } vip, err := vips.Create(lb.network, createOpts).Extract() diff --git a/pkg/controller/service/servicecontroller.go b/pkg/controller/service/servicecontroller.go index 69b644fce99..688283b015f 100644 --- a/pkg/controller/service/servicecontroller.go +++ b/pkg/controller/service/servicecontroller.go @@ -378,28 +378,14 @@ func (s *ServiceController) createExternalLoadBalancer(service *api.Service) err return err } name := s.loadBalancerName(service) - if len(service.Spec.ExternalIPs) > 0 { - for _, publicIP := range service.Spec.ExternalIPs { - // TODO: Make this actually work for multiple IPs by using different - // 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), - ports, hostsFromNodeList(&nodes), service.Spec.SessionAffinity) - if err != nil { - return err - } else { - service.Status.LoadBalancer = *status - } - break - } + status, err := s.balancer.EnsureTCPLoadBalancer(name, s.zone.Region, net.ParseIP(service.Spec.LoadBalancerIP), + ports, hostsFromNodeList(&nodes), service.Spec.SessionAffinity) + if err != nil { + return err } else { - status, err := s.balancer.EnsureTCPLoadBalancer(name, s.zone.Region, nil, - ports, hostsFromNodeList(&nodes), service.Spec.SessionAffinity) - if err != nil { - return err - } else { - service.Status.LoadBalancer = *status - } + service.Status.LoadBalancer = *status } + return nil } @@ -477,6 +463,9 @@ func needsUpdate(oldService *api.Service, newService *api.Service) bool { if !portsEqualForLB(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity { return true } + if !loadBalancerIPsAreEqual(oldService, newService) { + return true + } if len(oldService.Spec.ExternalIPs) != len(newService.Spec.ExternalIPs) { return true } @@ -689,3 +678,7 @@ func (s *ServiceController) lockedUpdateLoadBalancerHosts(service *api.Service, func wantsExternalLoadBalancer(service *api.Service) bool { return service.Spec.Type == api.ServiceTypeLoadBalancer } + +func loadBalancerIPsAreEqual(oldService, newService *api.Service) bool { + return oldService.Spec.LoadBalancerIP == newService.Spec.LoadBalancerIP +} diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index 790683bd893..06c50e98112 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -75,6 +75,7 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command { // TODO: remove create-external-load-balancer in code on or after Aug 25, 2016. 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().MarkDeprecated("create-external-load-balancer", "use --type=\"LoadBalancer\" instead") + cmd.Flags().String("load-balancer-ip", "", "IP to assign to to the Load Balancer. If empty, an ephemeral IP will be created and used(cloud-provider specific).") 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().Bool("dry-run", false, "If true, only print the object that would be sent, without creating it.") diff --git a/pkg/kubectl/service.go b/pkg/kubectl/service.go index 927fa1e820a..172dc6721ca 100644 --- a/pkg/kubectl/service.go +++ b/pkg/kubectl/service.go @@ -55,6 +55,7 @@ func paramNames() []GeneratorParam { {"labels", false}, {"external-ip", false}, {"create-external-load-balancer", false}, + {"load-balancer-ip", false}, {"type", false}, {"protocol", false}, {"container-port", false}, // alias of target-port @@ -149,6 +150,9 @@ func generate(genericParams map[string]interface{}) (runtime.Object, error) { if len(params["type"]) != 0 { service.Spec.Type = api.ServiceType(params["type"]) } + if service.Spec.Type == api.ServiceTypeLoadBalancer { + service.Spec.LoadBalancerIP = params["load-balancer-ip"] + } if len(params["session-affinity"]) != 0 { switch api.ServiceAffinity(params["session-affinity"]) { case api.ServiceAffinityNone: diff --git a/test/e2e/google_compute.go b/test/e2e/google_compute.go new file mode 100644 index 00000000000..270831ff9e8 --- /dev/null +++ b/test/e2e/google_compute.go @@ -0,0 +1,67 @@ +/* +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 e2e + +import ( + "fmt" + "github.com/golang/glog" + "os/exec" + "regexp" + "strings" +) + +func createGCEStaticIP(name string) (string, error) { + // gcloud compute --project "abshah-kubernetes-001" addresses create "test-static-ip" --region "us-central1" + // abshah@abhidesk:~/go/src/code.google.com/p/google-api-go-client/compute/v1$ gcloud compute --project "abshah-kubernetes-001" addresses create "test-static-ip" --region "us-central1" + // Created [https://www.googleapis.com/compute/v1/projects/abshah-kubernetes-001/regions/us-central1/addresses/test-static-ip]. + // NAME REGION ADDRESS STATUS + // test-static-ip us-central1 104.197.143.7 RESERVED + + output, err := exec.Command("gcloud", "compute", "addresses", "create", + name, "--project", testContext.CloudConfig.ProjectID, + "--region", "us-central1", "-q").CombinedOutput() + if err != nil { + return "", err + } + glog.Errorf("Creating static IP with name:%s in project: %s", name, testContext.CloudConfig.ProjectID) + text := string(output) + if strings.Contains(text, "RESERVED") { + r, _ := regexp.Compile("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+") + staticIP := r.FindString(text) + if staticIP == "" { + glog.Errorf("Static IP creation output is \n %s", text) + return "", fmt.Errorf("Static IP not found in gcloud compute command output") + } else { + return staticIP, nil + } + } else { + return "", fmt.Errorf("Static IP Could not be reserved.") + } +} + +func deleteGCEStaticIP(name string) error { + // gcloud compute --project "abshah-kubernetes-001" addresses create "test-static-ip" --region "us-central1" + // abshah@abhidesk:~/go/src/code.google.com/p/google-api-go-client/compute/v1$ gcloud compute --project "abshah-kubernetes-001" addresses create "test-static-ip" --region "us-central1" + // Created [https://www.googleapis.com/compute/v1/projects/abshah-kubernetes-001/regions/us-central1/addresses/test-static-ip]. + // NAME REGION ADDRESS STATUS + // test-static-ip us-central1 104.197.143.7 RESERVED + + _, err := exec.Command("gcloud", "compute", "addresses", "delete", + name, "--project", testContext.CloudConfig.ProjectID, + "--region", "us-central1", "-q").CombinedOutput() + return err +} diff --git a/test/e2e/service.go b/test/e2e/service.go index f6b054b645a..7602f5b2d3f 100644 --- a/test/e2e/service.go +++ b/test/e2e/service.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "github.com/golang/glog" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/kubernetes/pkg/api" @@ -418,6 +419,75 @@ var _ = Describe("Services", func() { testLoadBalancerReachable(ingress, inboundPort) }) + It("should be able to create a functioning external load balancer with user-provided load balancer ip", func() { + // requires ExternalLoadBalancer + SkipUnlessProviderIs("gce", "gke") + + serviceName := "lb-test-with-user-ip" + 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) + } + }() + + inboundPort := 3000 + + service := t.BuildServiceSpec() + service.Spec.Type = api.ServiceTypeLoadBalancer + service.Spec.Ports[0].Port = inboundPort + service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(80) + + By("creating an external static ip") + rand.Seed(time.Now().UTC().UnixNano()) + staticIPName := fmt.Sprintf("e2e-external-lb-test-%d", rand.Intn(65535)) + glog.Errorf("static ip name is %s", staticIPName) + loadBalancerIP, err := createGCEStaticIP(staticIPName) + Expect(err).NotTo(HaveOccurred()) + defer func() { + deleteGCEStaticIP(staticIPName) + }() + + service.Spec.LoadBalancerIP = loadBalancerIP + + 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] + Expect(ingress.IP).To(Equal(loadBalancerIP)) + 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.CreateWebserverRC(1) + + 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, inboundPort) + }) + It("should be able to create a functioning NodePort service", func() { serviceName := "nodeportservice-test" ns := namespaces[0]