Skip to content

Commit

Permalink
Create a LB for a K8S with the LB-IP provided by user.
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtfulCoder committed Sep 11, 2015
1 parent bfc6070 commit 44ce4aa
Show file tree
Hide file tree
Showing 20 changed files with 206 additions and 32 deletions.
4 changes: 4 additions & 0 deletions api/swagger-spec/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
},
Expand Down
1 change: 1 addition & 0 deletions contrib/completions/bash/kubectl
Original file line number Diff line number Diff line change
Expand Up @@ -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=")
Expand Down
4 changes: 4 additions & 0 deletions docs/man/man1/kubectl-expose.1
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/user-guide/kubectl/kubectl_expose.md
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand Down
7 changes: 6 additions & 1 deletion docs/user-guide/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions hack/verify-flags/known-flags.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkg/api/deep_copy_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
13 changes: 10 additions & 3 deletions pkg/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand All @@ -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"`
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/api/v1/conversion_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions pkg/api/v1/deep_copy_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
7 changes: 7 additions & 0 deletions pkg/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions pkg/api/v1/types_swagger_doc_generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/cloudprovider/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion pkg/cloudprovider/providers/gce/gce.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions pkg/cloudprovider/providers/openstack/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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()
Expand Down
33 changes: 13 additions & 20 deletions pkg/controller/service/servicecontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
1 change: 1 addition & 0 deletions pkg/kubectl/cmd/expose.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
4 changes: 4 additions & 0 deletions pkg/kubectl/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
67 changes: 67 additions & 0 deletions test/e2e/google_compute.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 44ce4aa

Please sign in to comment.