From 973c2e481910429faaa082197dfc0f3a6817de2b Mon Sep 17 00:00:00 2001 From: Justin Santa Barbara Date: Fri, 22 May 2015 17:49:26 -0400 Subject: [PATCH] Add Type to ServiceSpec: ClusterIP or LoadBalancer --- contrib/completions/bash/kubectl | 1 + docs/kubectl_expose.md | 5 +- docs/man/man1/kubectl-expose.1 | 6 +- examples/simple-nginx.md | 2 +- pkg/api/rest/update_test.go | 1 + pkg/api/testing/fuzzer.go | 4 + pkg/api/types.go | 19 +++- pkg/api/v1/conversion_generated.go | 4 +- pkg/api/v1/defaults.go | 3 + pkg/api/v1/defaults_test.go | 5 +- pkg/api/v1/types.go | 18 +++- pkg/api/v1beta1/conversion.go | 18 +++- pkg/api/v1beta1/defaults.go | 9 ++ pkg/api/v1beta1/defaults_test.go | 15 ++- pkg/api/v1beta1/types.go | 17 ++++ pkg/api/v1beta2/conversion.go | 18 +++- pkg/api/v1beta2/defaults.go | 9 ++ pkg/api/v1beta2/defaults_test.go | 15 ++- pkg/api/v1beta2/types.go | 17 ++++ pkg/api/v1beta3/conversion.go | 91 +++++++++++++++++++ pkg/api/v1beta3/conversion_generated.go | 74 --------------- pkg/api/v1beta3/defaults.go | 9 ++ pkg/api/v1beta3/defaults_test.go | 15 ++- pkg/api/v1beta3/types.go | 17 ++++ pkg/api/validation/validation.go | 10 +- pkg/api/validation/validation_test.go | 47 +++++++++- .../servicecontroller/servicecontroller.go | 14 ++- .../servicecontroller_test.go | 32 +++---- pkg/kubectl/cmd/expose.go | 8 +- pkg/kubectl/cmd/get_test.go | 1 + pkg/kubectl/cmd/util/helpers_test.go | 2 + pkg/kubectl/describe.go | 1 + pkg/kubectl/service.go | 6 +- pkg/kubectl/service_test.go | 4 +- pkg/registry/etcd/etcd_test.go | 1 + pkg/registry/service/rest_test.go | 54 +++++++---- test/e2e/service.go | 8 +- 37 files changed, 434 insertions(+), 146 deletions(-) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 208229bb671..b137c008582 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -629,6 +629,7 @@ _kubectl_expose() flags+=("--target-port=") flags+=("--template=") two_word_flags+=("-t") + flags+=("--type=") must_have_one_flag=() must_have_one_flag+=("--port=") diff --git a/docs/kubectl_expose.md b/docs/kubectl_expose.md index 82c0a4d426f..95d8eda686e 100644 --- a/docs/kubectl_expose.md +++ b/docs/kubectl_expose.md @@ -12,7 +12,7 @@ selector for a new Service on the specified port. If no labels are specified, th re-use the labels from the resource it exposes. ``` -kubectl expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--create-external-load-balancer=bool] +kubectl expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type] ``` ### Examples @@ -32,7 +32,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream ``` --container-port="": Synonym for --target-port - --create-external-load-balancer=false: If true, create an external load balancer for this service. Implementation is cloud provider dependent. Default is 'false'. + --create-external-load-balancer=false: If true, create an external load balancer for this service (trumped by --type). Implementation is cloud provider dependent. Default is 'false'. --dry-run=false: If true, only print the object that would be sent, without creating it. --generator="service/v1": The name of the API generator to use. Default is 'service/v1'. -h, --help=false: help for expose @@ -48,6 +48,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream --selector="": A label selector to use for this service. If empty (the default) infer the selector from the replication controller. --target-port="": Name or number for the port on the container that the service should direct traffic to. Optional. -t, --template="": Template string or path to template file to use when -o=template or -o=templatefile. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview] + --type="": Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP' unless --create-external-load-balancer is specified. ``` ### Options inherited from parent commands diff --git a/docs/man/man1/kubectl-expose.1 b/docs/man/man1/kubectl-expose.1 index a24642dd8cb..15fc38cd831 100644 --- a/docs/man/man1/kubectl-expose.1 +++ b/docs/man/man1/kubectl-expose.1 @@ -28,7 +28,7 @@ re\-use the labels from the resource it exposes. .PP \fB\-\-create\-external\-load\-balancer\fP=false - If true, create an external load balancer for this service. Implementation is cloud provider dependent. Default is 'false'. + If true, create an external load balancer for this service (trumped by \-\-type). Implementation is cloud provider dependent. Default is 'false'. .PP \fB\-\-dry\-run\fP=false @@ -91,6 +91,10 @@ re\-use the labels from the resource it exposes. Template string or path to template file to use when \-o=template or \-o=templatefile. The template format is golang templates [ \[la]http://golang.org/pkg/text/template/#pkg-overview\[ra]] +.PP +\fB\-\-type\fP="" + Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP' unless \-\-create\-external\-load\-balancer is specified. + .SH OPTIONS INHERITED FROM PARENT COMMANDS .PP diff --git a/examples/simple-nginx.md b/examples/simple-nginx.md index 60b866e54e4..d0eb0ec1fea 100644 --- a/examples/simple-nginx.md +++ b/examples/simple-nginx.md @@ -35,7 +35,7 @@ On some platforms (for example Google Compute Engine) the kubectl command can in to do this run: ```bash -kubectl expose rc my-nginx --port=80 --create-external-load-balancer +kubectl expose rc my-nginx --port=80 --type=LoadBalancer ``` This should print the service that has been created, and map an external IP address to the service. diff --git a/pkg/api/rest/update_test.go b/pkg/api/rest/update_test.go index fa8d8581b94..049a2a250a4 100644 --- a/pkg/api/rest/update_test.go +++ b/pkg/api/rest/update_test.go @@ -35,6 +35,7 @@ func makeValidService() api.Service { Spec: api.ServiceSpec{ Selector: map[string]string{"key": "val"}, SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}}, }, } diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index fbe24679b5f..fb0a4b20a19 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -186,6 +186,10 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { types := []api.ServiceAffinity{api.ServiceAffinityClientIP, api.ServiceAffinityNone} *p = types[c.Rand.Intn(len(types))] }, + func(p *api.ServiceType, c fuzz.Continue) { + types := []api.ServiceType{api.ServiceTypeClusterIP, api.ServiceTypeLoadBalancer} + *p = types[c.Rand.Intn(len(types))] + }, func(ct *api.Container, c fuzz.Continue) { c.FuzzNoCustom(ct) // fuzz self without calling this function again ct.TerminationMessagePath = "/" + ct.TerminationMessagePath // Must be non-empty diff --git a/pkg/api/types.go b/pkg/api/types.go index e610927e6cb..02fefea78bc 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1011,6 +1011,20 @@ const ( ServiceAffinityNone ServiceAffinity = "None" ) +// Service Type string describes ingress methods for a service +type ServiceType string + +const ( + // ServiceTypeClusterIP means a service will only be accessible inside the + // cluster, via the portal IP. + ServiceTypeClusterIP ServiceType = "ClusterIP" + + // ServiceTypeLoadBalancer means a service will be exposed via an + // external load balancer (if the cloud provider supports it), in addition + // to 'NodePort' type. + ServiceTypeLoadBalancer ServiceType = "LoadBalancer" +) + // ServiceStatus represents the current status of a service type ServiceStatus struct { // LoadBalancer contains the current status of the load-balancer, @@ -1054,8 +1068,9 @@ type ServiceSpec struct { // None can be specified for headless services when proxying is not required PortalIP string `json:"portalIP,omitempty"` - // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. - CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty"` + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty"` + // PublicIPs are used by external load balancers, or can be set by // users to handle external traffic that arrives at a node. // For load balancers, the publicIP will usually be the IP address of the load balancer, diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 10615e269ce..7caa4e574ca 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -2097,7 +2097,7 @@ func convert_api_ServiceSpec_To_v1_ServiceSpec(in *api.ServiceSpec, out *Service out.Selector = nil } out.PortalIP = in.PortalIP - out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer + out.Type = ServiceType(in.Type) if in.PublicIPs != nil { out.PublicIPs = make([]string, len(in.PublicIPs)) for i := range in.PublicIPs { @@ -4352,7 +4352,7 @@ func convert_v1_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.Service out.Selector = nil } out.PortalIP = in.PortalIP - out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer + out.Type = api.ServiceType(in.Type) if in.PublicIPs != nil { out.PublicIPs = make([]string, len(in.PublicIPs)) for i := range in.PublicIPs { diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index d7814b04453..cfc8a1fdec9 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -75,6 +75,9 @@ func addDefaultingFuncs() { if obj.SessionAffinity == "" { obj.SessionAffinity = ServiceAffinityNone } + if obj.Type == "" { + obj.Type = ServiceTypeClusterIP + } for i := range obj.Ports { sp := &obj.Ports[i] if sp.Protocol == "" { diff --git a/pkg/api/v1/defaults_test.go b/pkg/api/v1/defaults_test.go index 17078e31ce4..c13c2f7fc05 100644 --- a/pkg/api/v1/defaults_test.go +++ b/pkg/api/v1/defaults_test.go @@ -233,7 +233,10 @@ func TestSetDefaultService(t *testing.T) { obj2 := roundTrip(t, runtime.Object(svc)) svc2 := obj2.(*versioned.Service) if svc2.Spec.SessionAffinity != versioned.ServiceAffinityNone { - t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity) + t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity) + } + if svc2.Spec.Type != versioned.ServiceTypeClusterIP { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Spec.Type) } } diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 8147eb5d69a..520b922c620 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -993,6 +993,20 @@ const ( ServiceAffinityNone ServiceAffinity = "None" ) +// Service Type string describes ingress methods for a service +type ServiceType string + +const ( + // ServiceTypeClusterIP means a service will only be accessible inside the + // cluster, via the portal IP. + ServiceTypeClusterIP ServiceType = "ClusterIP" + + // ServiceTypeLoadBalancer means a service will be exposed via an + // external load balancer (if the cloud provider supports it), in addition + // to 'NodePort' type. + ServiceTypeLoadBalancer ServiceType = "LoadBalancer" +) + // ServiceStatus represents the current status of a service type ServiceStatus struct { // LoadBalancer contains the current status of the load-balancer, @@ -1034,8 +1048,8 @@ type ServiceSpec struct { // None can be specified for headless services when proxying is not required PortalIP string `json:"portalIP,omitempty description: IP address of the service; usually assigned by the system; if specified, it will be allocated to the service if unused, and creation of the service will fail otherwise; cannot be updated; 'None' can be specified for a headless service when proxying is not required"` - // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. - CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"` // PublicIPs are used by external load balancers, or can be set by // users to handle external traffic that arrives at a node. diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 15979c24827..199fd85b9a0 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -780,7 +780,6 @@ func addConversionFuncs() { if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil { return err } - out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer out.PublicIPs = in.Spec.PublicIPs out.PortalIP = in.Spec.PortalIP if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil { @@ -791,6 +790,10 @@ func addConversionFuncs() { return err } + if err := s.Convert(&in.Spec.Type, &out.Type, 0); err != nil { + return err + } + return nil }, func(in *Service, out *api.Service, s conversion.Scope) error { @@ -827,7 +830,6 @@ func addConversionFuncs() { if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil { return err } - out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer out.Spec.PublicIPs = in.PublicIPs out.Spec.PortalIP = in.PortalIP if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil { @@ -850,6 +852,18 @@ func addConversionFuncs() { return err } + typeIn := in.Type + if typeIn == "" { + if in.CreateExternalLoadBalancer { + typeIn = ServiceTypeLoadBalancer + } else { + typeIn = ServiceTypeClusterIP + } + } + if err := s.Convert(&typeIn, &out.Spec.Type, 0); err != nil { + return err + } + return nil }, diff --git a/pkg/api/v1beta1/defaults.go b/pkg/api/v1beta1/defaults.go index 31b9784c699..e8405597a99 100644 --- a/pkg/api/v1beta1/defaults.go +++ b/pkg/api/v1beta1/defaults.go @@ -76,6 +76,15 @@ func addDefaultingFuncs() { if obj.SessionAffinity == "" { obj.SessionAffinity = ServiceAffinityNone } + if obj.Type == "" { + if obj.CreateExternalLoadBalancer { + obj.Type = ServiceTypeLoadBalancer + } else { + obj.Type = ServiceTypeClusterIP + } + } else if obj.Type == ServiceTypeLoadBalancer { + obj.CreateExternalLoadBalancer = true + } for i := range obj.Ports { sp := &obj.Ports[i] if sp.Protocol == "" { diff --git a/pkg/api/v1beta1/defaults_test.go b/pkg/api/v1beta1/defaults_test.go index 48a1b88e9d7..d254396b634 100644 --- a/pkg/api/v1beta1/defaults_test.go +++ b/pkg/api/v1beta1/defaults_test.go @@ -150,7 +150,20 @@ func TestSetDefaultService(t *testing.T) { t.Errorf("Expected default protocol :%s, got: %s", versioned.ProtocolTCP, svc2.Protocol) } if svc2.SessionAffinity != versioned.ServiceAffinityNone { - t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity) + t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity) + } + if svc2.Type != versioned.ServiceTypeClusterIP { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Type) + } +} + +func TestSetDefaultServiceWithLoadbalancer(t *testing.T) { + svc := &versioned.Service{} + svc.CreateExternalLoadBalancer = true + obj2 := roundTrip(t, runtime.Object(svc)) + svc2 := obj2.(*versioned.Service) + if svc2.Type != versioned.ServiceTypeLoadBalancer { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeLoadBalancer, svc2.Type) } } diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index ad0127ab684..08484ef524c 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -835,6 +835,20 @@ const ( ServiceAffinityNone ServiceAffinity = "None" ) +// Service Type string describes ingress methods for a service +type ServiceType string + +const ( + // ServiceTypeClusterIP means a service will only be accessible inside the + // cluster, via the portal IP. + ServiceTypeClusterIP ServiceType = "ClusterIP" + + // ServiceTypeLoadBalancer means a service will be exposed via an + // external load balancer (if the cloud provider supports it), in addition + // to 'NodePort' type. + ServiceTypeLoadBalancer ServiceType = "LoadBalancer" +) + const ( // PortalIPNone - do not assign a portal IP // no proxying required and no environment variables should be created for pods @@ -873,6 +887,9 @@ type Service struct { // An external load balancer should be set up via the cloud-provider CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"` + // PublicIPs are used by external load balancers, or can be set by // users to handle external traffic that arrives at a node. PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index de99dd8aacc..a45e1909ae7 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -702,7 +702,6 @@ func addConversionFuncs() { if err := s.Convert(&in.Spec.Selector, &out.Selector, 0); err != nil { return err } - out.CreateExternalLoadBalancer = in.Spec.CreateExternalLoadBalancer out.PublicIPs = in.Spec.PublicIPs out.PortalIP = in.Spec.PortalIP if err := s.Convert(&in.Spec.SessionAffinity, &out.SessionAffinity, 0); err != nil { @@ -713,6 +712,10 @@ func addConversionFuncs() { return err } + if err := s.Convert(&in.Spec.Type, &out.Type, 0); err != nil { + return err + } + return nil }, func(in *Service, out *api.Service, s conversion.Scope) error { @@ -749,7 +752,6 @@ func addConversionFuncs() { if err := s.Convert(&in.Selector, &out.Spec.Selector, 0); err != nil { return err } - out.Spec.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer out.Spec.PublicIPs = in.PublicIPs out.Spec.PortalIP = in.PortalIP if err := s.Convert(&in.SessionAffinity, &out.Spec.SessionAffinity, 0); err != nil { @@ -772,6 +774,18 @@ func addConversionFuncs() { return err } + typeIn := in.Type + if typeIn == "" { + if in.CreateExternalLoadBalancer { + typeIn = ServiceTypeLoadBalancer + } else { + typeIn = ServiceTypeClusterIP + } + } + if err := s.Convert(&typeIn, &out.Spec.Type, 0); err != nil { + return err + } + return nil }, diff --git a/pkg/api/v1beta2/defaults.go b/pkg/api/v1beta2/defaults.go index 16bcc737b84..d0e7bffc661 100644 --- a/pkg/api/v1beta2/defaults.go +++ b/pkg/api/v1beta2/defaults.go @@ -77,6 +77,15 @@ func addDefaultingFuncs() { if obj.SessionAffinity == "" { obj.SessionAffinity = ServiceAffinityNone } + if obj.Type == "" { + if obj.CreateExternalLoadBalancer { + obj.Type = ServiceTypeLoadBalancer + } else { + obj.Type = ServiceTypeClusterIP + } + } else if obj.Type == ServiceTypeLoadBalancer { + obj.CreateExternalLoadBalancer = true + } for i := range obj.Ports { sp := &obj.Ports[i] if sp.Protocol == "" { diff --git a/pkg/api/v1beta2/defaults_test.go b/pkg/api/v1beta2/defaults_test.go index 5dda50673fd..0a9224b5f8f 100644 --- a/pkg/api/v1beta2/defaults_test.go +++ b/pkg/api/v1beta2/defaults_test.go @@ -150,7 +150,20 @@ func TestSetDefaultService(t *testing.T) { t.Errorf("Expected default protocol :%s, got: %s", versioned.ProtocolTCP, svc2.Protocol) } if svc2.SessionAffinity != versioned.ServiceAffinityNone { - t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity) + t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.SessionAffinity) + } + if svc2.Type != versioned.ServiceTypeClusterIP { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Type) + } +} + +func TestSetDefaultServiceWithLoadbalancer(t *testing.T) { + svc := &versioned.Service{} + svc.CreateExternalLoadBalancer = true + obj2 := roundTrip(t, runtime.Object(svc)) + svc2 := obj2.(*versioned.Service) + if svc2.Type != versioned.ServiceTypeLoadBalancer { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeLoadBalancer, svc2.Type) } } diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 2031451dbdf..74cf5676161 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -837,6 +837,20 @@ const ( ServiceAffinityNone ServiceAffinity = "None" ) +// Service Type string describes ingress methods for a service +type ServiceType string + +const ( + // ServiceTypeClusterIP means a service will only be accessible inside the + // cluster, via the portal IP. + ServiceTypeClusterIP ServiceType = "ClusterIP" + + // ServiceTypeLoadBalancer means a service will be exposed via an + // external load balancer (if the cloud provider supports it), in addition + // to 'NodePort' type. + ServiceTypeLoadBalancer ServiceType = "LoadBalancer" +) + const ( // PortalIPNone - do not assign a portal IP // no proxying required and no environment variables should be created for pods @@ -877,6 +891,9 @@ type Service struct { // An external load balancer should be set up via the cloud-provider CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"` + // PublicIPs are used by external load balancers, or can be set by // users to handle external traffic that arrives at a node. PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` diff --git a/pkg/api/v1beta3/conversion.go b/pkg/api/v1beta3/conversion.go index 9dd70e539ca..87817b130c2 100644 --- a/pkg/api/v1beta3/conversion.go +++ b/pkg/api/v1beta3/conversion.go @@ -29,6 +29,8 @@ func addConversionFuncs() { err := api.Scheme.AddConversionFuncs( convert_v1beta3_Container_To_api_Container, convert_api_Container_To_v1beta3_Container, + convert_v1beta3_ServiceSpec_To_api_ServiceSpec, + convert_api_ServiceSpec_To_v1beta3_ServiceSpec, ) if err != nil { // If one of the conversion functions is malformed, detect it immediately. @@ -329,3 +331,92 @@ func convert_api_Container_To_v1beta3_Container(in *api.Container, out *Containe } return nil } + +func convert_v1beta3_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.ServiceSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*ServiceSpec))(in) + } + if in.Ports != nil { + out.Ports = make([]api.ServicePort, len(in.Ports)) + for i := range in.Ports { + if err := convert_v1beta3_ServicePort_To_api_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil { + return err + } + } + } else { + out.Ports = nil + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + out.PortalIP = in.PortalIP + + typeIn := in.Type + if typeIn == "" { + if in.CreateExternalLoadBalancer { + typeIn = ServiceTypeLoadBalancer + } else { + typeIn = ServiceTypeClusterIP + } + } + if err := s.Convert(&typeIn, &out.Type, 0); err != nil { + return err + } + + if in.PublicIPs != nil { + out.PublicIPs = make([]string, len(in.PublicIPs)) + for i := range in.PublicIPs { + out.PublicIPs[i] = in.PublicIPs[i] + } + } else { + out.PublicIPs = nil + } + out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity) + return nil +} + +func convert_api_ServiceSpec_To_v1beta3_ServiceSpec(in *api.ServiceSpec, out *ServiceSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.ServiceSpec))(in) + } + if in.Ports != nil { + out.Ports = make([]ServicePort, len(in.Ports)) + for i := range in.Ports { + if err := convert_api_ServicePort_To_v1beta3_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil { + return err + } + } + } else { + out.Ports = nil + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + out.PortalIP = in.PortalIP + + if err := s.Convert(&in.Type, &out.Type, 0); err != nil { + return err + } + out.CreateExternalLoadBalancer = in.Type == api.ServiceTypeLoadBalancer + + if in.PublicIPs != nil { + out.PublicIPs = make([]string, len(in.PublicIPs)) + for i := range in.PublicIPs { + out.PublicIPs[i] = in.PublicIPs[i] + } + } else { + out.PublicIPs = nil + } + out.SessionAffinity = ServiceAffinity(in.SessionAffinity) + return nil +} diff --git a/pkg/api/v1beta3/conversion_generated.go b/pkg/api/v1beta3/conversion_generated.go index 593ce270213..8ec294103e1 100644 --- a/pkg/api/v1beta3/conversion_generated.go +++ b/pkg/api/v1beta3/conversion_generated.go @@ -2013,42 +2013,6 @@ func convert_api_ServicePort_To_v1beta3_ServicePort(in *api.ServicePort, out *Se return nil } -func convert_api_ServiceSpec_To_v1beta3_ServiceSpec(in *api.ServiceSpec, out *ServiceSpec, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*api.ServiceSpec))(in) - } - if in.Ports != nil { - out.Ports = make([]ServicePort, len(in.Ports)) - for i := range in.Ports { - if err := convert_api_ServicePort_To_v1beta3_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil { - return err - } - } - } else { - out.Ports = nil - } - if in.Selector != nil { - out.Selector = make(map[string]string) - for key, val := range in.Selector { - out.Selector[key] = val - } - } else { - out.Selector = nil - } - out.PortalIP = in.PortalIP - out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer - if in.PublicIPs != nil { - out.PublicIPs = make([]string, len(in.PublicIPs)) - for i := range in.PublicIPs { - out.PublicIPs[i] = in.PublicIPs[i] - } - } else { - out.PublicIPs = nil - } - out.SessionAffinity = ServiceAffinity(in.SessionAffinity) - return nil -} - func convert_api_ServiceStatus_To_v1beta3_ServiceStatus(in *api.ServiceStatus, out *ServiceStatus, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.ServiceStatus))(in) @@ -4237,42 +4201,6 @@ func convert_v1beta3_ServicePort_To_api_ServicePort(in *ServicePort, out *api.Se return nil } -func convert_v1beta3_ServiceSpec_To_api_ServiceSpec(in *ServiceSpec, out *api.ServiceSpec, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*ServiceSpec))(in) - } - if in.Ports != nil { - out.Ports = make([]api.ServicePort, len(in.Ports)) - for i := range in.Ports { - if err := convert_v1beta3_ServicePort_To_api_ServicePort(&in.Ports[i], &out.Ports[i], s); err != nil { - return err - } - } - } else { - out.Ports = nil - } - if in.Selector != nil { - out.Selector = make(map[string]string) - for key, val := range in.Selector { - out.Selector[key] = val - } - } else { - out.Selector = nil - } - out.PortalIP = in.PortalIP - out.CreateExternalLoadBalancer = in.CreateExternalLoadBalancer - if in.PublicIPs != nil { - out.PublicIPs = make([]string, len(in.PublicIPs)) - for i := range in.PublicIPs { - out.PublicIPs[i] = in.PublicIPs[i] - } - } else { - out.PublicIPs = nil - } - out.SessionAffinity = api.ServiceAffinity(in.SessionAffinity) - return nil -} - func convert_v1beta3_ServiceStatus_To_api_ServiceStatus(in *ServiceStatus, out *api.ServiceStatus, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*ServiceStatus))(in) @@ -4577,7 +4505,6 @@ func init() { convert_api_ServiceAccount_To_v1beta3_ServiceAccount, convert_api_ServiceList_To_v1beta3_ServiceList, convert_api_ServicePort_To_v1beta3_ServicePort, - convert_api_ServiceSpec_To_v1beta3_ServiceSpec, convert_api_ServiceStatus_To_v1beta3_ServiceStatus, convert_api_Service_To_v1beta3_Service, convert_api_StatusCause_To_v1beta3_StatusCause, @@ -4690,7 +4617,6 @@ func init() { convert_v1beta3_ServiceAccount_To_api_ServiceAccount, convert_v1beta3_ServiceList_To_api_ServiceList, convert_v1beta3_ServicePort_To_api_ServicePort, - convert_v1beta3_ServiceSpec_To_api_ServiceSpec, convert_v1beta3_ServiceStatus_To_api_ServiceStatus, convert_v1beta3_Service_To_api_Service, convert_v1beta3_StatusCause_To_api_StatusCause, diff --git a/pkg/api/v1beta3/defaults.go b/pkg/api/v1beta3/defaults.go index 674f7797ab9..f387a45f946 100644 --- a/pkg/api/v1beta3/defaults.go +++ b/pkg/api/v1beta3/defaults.go @@ -73,6 +73,15 @@ func addDefaultingFuncs() { if obj.SessionAffinity == "" { obj.SessionAffinity = ServiceAffinityNone } + if obj.Type == "" { + if obj.CreateExternalLoadBalancer { + obj.Type = ServiceTypeLoadBalancer + } else { + obj.Type = ServiceTypeClusterIP + } + } else if obj.Type == ServiceTypeLoadBalancer { + obj.CreateExternalLoadBalancer = true + } for i := range obj.Ports { sp := &obj.Ports[i] if sp.Protocol == "" { diff --git a/pkg/api/v1beta3/defaults_test.go b/pkg/api/v1beta3/defaults_test.go index 88094390646..6d5a411979b 100644 --- a/pkg/api/v1beta3/defaults_test.go +++ b/pkg/api/v1beta3/defaults_test.go @@ -160,7 +160,20 @@ func TestSetDefaultService(t *testing.T) { obj2 := roundTrip(t, runtime.Object(svc)) svc2 := obj2.(*versioned.Service) if svc2.Spec.SessionAffinity != versioned.ServiceAffinityNone { - t.Errorf("Expected default sesseion affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity) + t.Errorf("Expected default session affinity type:%s, got: %s", versioned.ServiceAffinityNone, svc2.Spec.SessionAffinity) + } + if svc2.Spec.Type != versioned.ServiceTypeClusterIP { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeClusterIP, svc2.Spec.Type) + } +} + +func TestSetDefaultServiceWithLoadbalancer(t *testing.T) { + svc := &versioned.Service{} + svc.Spec.CreateExternalLoadBalancer = true + obj2 := roundTrip(t, runtime.Object(svc)) + svc2 := obj2.(*versioned.Service) + if svc2.Spec.Type != versioned.ServiceTypeLoadBalancer { + t.Errorf("Expected default type:%s, got: %s", versioned.ServiceTypeLoadBalancer, svc2.Spec.Type) } } diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index f8d4a574ead..4c5757410cc 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -997,6 +997,20 @@ const ( ServiceAffinityNone ServiceAffinity = "None" ) +// Service Type string describes ingress methods for a service +type ServiceType string + +const ( + // ServiceTypeClusterIP means a service will only be accessible inside the + // cluster, via the portal IP. + ServiceTypeClusterIP ServiceType = "ClusterIP" + + // ServiceTypeLoadBalancer means a service will be exposed via an + // external load balancer (if the cloud provider supports it), in addition + // to 'NodePort' type. + ServiceTypeLoadBalancer ServiceType = "LoadBalancer" +) + // ServiceStatus represents the current status of a service type ServiceStatus struct { // LoadBalancer contains the current status of the load-balancer, @@ -1041,6 +1055,9 @@ type ServiceSpec struct { // CreateExternalLoadBalancer indicates whether a load balancer should be created for this service. CreateExternalLoadBalancer bool `json:"createExternalLoadBalancer,omitempty" description:"set up a cloud-provider-specific load balancer on an external IP"` + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty" description:"type of this service; must be ClusterIP, NodePort, or LoadBalancer; defaults to ClusterIP"` + // PublicIPs are used by external load balancers, or can be set by // users to handle external traffic that arrives at a node. PublicIPs []string `json:"publicIPs,omitempty" description:"externally visible IPs (e.g. load balancers) that should be proxied to this service"` diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 81e22d58f80..6bf657527ce 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1032,6 +1032,8 @@ func ValidatePodTemplateUpdate(newPod, oldPod *api.PodTemplate) errs.ValidationE } var supportedSessionAffinityType = util.NewStringSet(string(api.ServiceAffinityClientIP), string(api.ServiceAffinityNone)) +var supportedServiceType = util.NewStringSet(string(api.ServiceTypeClusterIP), + string(api.ServiceTypeLoadBalancer)) // ValidateService tests if required fields in the service are set. func ValidateService(service *api.Service) errs.ValidationErrorList { @@ -1070,7 +1072,13 @@ func ValidateService(service *api.Service) errs.ValidationErrorList { } } - if service.Spec.CreateExternalLoadBalancer { + if service.Spec.Type == "" { + allErrs = append(allErrs, errs.NewFieldRequired("spec.type")) + } else if !supportedServiceType.Has(string(service.Spec.Type)) { + allErrs = append(allErrs, errs.NewFieldNotSupported("spec.type", service.Spec.Type)) + } + + if service.Spec.Type == api.ServiceTypeLoadBalancer { for i := range service.Spec.Ports { if service.Spec.Ports[i].Protocol != api.ProtocolTCP { allErrs = append(allErrs, errs.NewFieldInvalid("spec.ports", service.Spec.Ports[i], "cannot create an external load balancer with non-TCP ports")) diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 7d429ca9d9f..6a455a59bf9 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -1427,6 +1427,7 @@ func makeValidService() api.Service { Spec: api.ServiceSpec{ Selector: map[string]string{"key": "val"}, SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{Name: "p", Protocol: "TCP", Port: 8675}}, }, } @@ -1522,6 +1523,13 @@ func TestValidateService(t *testing.T) { }, numErrs: 1, }, + { + name: "missing type", + tweakSvc: func(s *api.Service) { + s.Spec.Type = "" + }, + numErrs: 1, + }, { name: "missing ports", tweakSvc: func(s *api.Service) { @@ -1632,7 +1640,7 @@ func TestValidateService(t *testing.T) { { name: "invalid load balancer protocol 1", tweakSvc: func(s *api.Service) { - s.Spec.CreateExternalLoadBalancer = true + s.Spec.Type = api.ServiceTypeLoadBalancer s.Spec.Ports[0].Protocol = "UDP" }, numErrs: 1, @@ -1640,7 +1648,7 @@ func TestValidateService(t *testing.T) { { name: "invalid load balancer protocol 2", tweakSvc: func(s *api.Service) { - s.Spec.CreateExternalLoadBalancer = true + s.Spec.Type = api.ServiceTypeLoadBalancer s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "UDP"}) }, numErrs: 1, @@ -1683,16 +1691,31 @@ func TestValidateService(t *testing.T) { numErrs: 0, }, { - name: "valid external load balancer", + name: "valid visbility - cluster", tweakSvc: func(s *api.Service) { - s.Spec.CreateExternalLoadBalancer = true + s.Spec.Type = api.ServiceTypeClusterIP + }, + numErrs: 0, + }, + { + name: "valid visbility - loadbalancer", + tweakSvc: func(s *api.Service) { + s.Spec.Type = api.ServiceTypeLoadBalancer + }, + numErrs: 0, + }, + { + name: "valid type loadbalancer 2 ports", + tweakSvc: func(s *api.Service) { + s.Spec.Type = api.ServiceTypeLoadBalancer + s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"}) }, numErrs: 0, }, { name: "valid external load balancer 2 ports", tweakSvc: func(s *api.Service) { - s.Spec.CreateExternalLoadBalancer = true + s.Spec.Type = api.ServiceTypeLoadBalancer s.Spec.Ports = append(s.Spec.Ports, api.ServicePort{Name: "q", Port: 12345, Protocol: "TCP"}) }, numErrs: 0, @@ -2458,6 +2481,20 @@ func TestValidateServiceUpdate(t *testing.T) { }, numErrs: 1, }, + { + name: "change type", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Spec.Type = api.ServiceTypeLoadBalancer + }, + numErrs: 0, + }, + { + name: "remove type", + tweakSvc: func(oldSvc, newSvc *api.Service) { + newSvc.Spec.Type = "" + }, + numErrs: 1, + }, } for _, tc := range testCases { diff --git a/pkg/cloudprovider/servicecontroller/servicecontroller.go b/pkg/cloudprovider/servicecontroller/servicecontroller.go index de5664cd6c2..170ab20da9b 100644 --- a/pkg/cloudprovider/servicecontroller/servicecontroller.go +++ b/pkg/cloudprovider/servicecontroller/servicecontroller.go @@ -231,7 +231,7 @@ func (s *ServiceController) createLoadBalancerIfNeeded(namespacedName types.Name if cachedService != nil { // If the service already exists but needs to be updated, delete it so that // we can recreate it cleanly. - if cachedService.Spec.CreateExternalLoadBalancer { + if wantsExternalLoadBalancer(cachedService) { glog.Infof("Deleting existing load balancer for service %s that needs an updated load balancer.", namespacedName) if err := s.balancer.EnsureTCPLoadBalancerDeleted(s.loadBalancerName(cachedService), s.zone.Region); err != nil { return err, retryable @@ -256,7 +256,7 @@ func (s *ServiceController) createLoadBalancerIfNeeded(namespacedName types.Name } } - if !service.Spec.CreateExternalLoadBalancer { + if !wantsExternalLoadBalancer(service) { glog.Infof("Not creating LB for service %s that doesn't want one.", namespacedName) return nil, notRetryable } @@ -404,10 +404,10 @@ func (s *serviceCache) delete(serviceName string) { } func needsUpdate(oldService *api.Service, newService *api.Service) bool { - if !oldService.Spec.CreateExternalLoadBalancer && !newService.Spec.CreateExternalLoadBalancer { + if !wantsExternalLoadBalancer(oldService) && !wantsExternalLoadBalancer(newService) { return false } - if oldService.Spec.CreateExternalLoadBalancer != newService.Spec.CreateExternalLoadBalancer { + if wantsExternalLoadBalancer(oldService) != wantsExternalLoadBalancer(newService) { return true } if !portsEqual(oldService, newService) || oldService.Spec.SessionAffinity != newService.Spec.SessionAffinity { @@ -561,7 +561,7 @@ func (s *ServiceController) updateLoadBalancerHosts(services []*cachedService, h // Updates the external load balancer of a service, assuming we hold the mutex // associated with the service. func (s *ServiceController) lockedUpdateLoadBalancerHosts(service *api.Service, hosts []string) error { - if !service.Spec.CreateExternalLoadBalancer { + if !wantsExternalLoadBalancer(service) { return nil } @@ -579,3 +579,7 @@ func (s *ServiceController) lockedUpdateLoadBalancerHosts(service *api.Service, } return err } + +func wantsExternalLoadBalancer(service *api.Service) bool { + return service.Spec.Type == api.ServiceTypeLoadBalancer +} diff --git a/pkg/cloudprovider/servicecontroller/servicecontroller_test.go b/pkg/cloudprovider/servicecontroller/servicecontroller_test.go index b54f98f2b29..f7a1513100e 100644 --- a/pkg/cloudprovider/servicecontroller/servicecontroller_test.go +++ b/pkg/cloudprovider/servicecontroller/servicecontroller_test.go @@ -28,8 +28,8 @@ import ( const region = "us-central" -func newService(name string, uid types.UID, external bool) *api.Service { - return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: "namespace", UID: uid}, Spec: api.ServiceSpec{CreateExternalLoadBalancer: external}} +func newService(name string, uid types.UID, serviceType api.ServiceType) *api.Service { + return &api.Service{ObjectMeta: api.ObjectMeta{Name: name, Namespace: "namespace", UID: uid}, Spec: api.ServiceSpec{Type: serviceType}} } func TestCreateExternalLoadBalancer(t *testing.T) { @@ -45,7 +45,7 @@ func TestCreateExternalLoadBalancer(t *testing.T) { Namespace: "default", }, Spec: api.ServiceSpec{ - CreateExternalLoadBalancer: false, + Type: api.ServiceTypeClusterIP, }, }, expectErr: false, @@ -62,7 +62,7 @@ func TestCreateExternalLoadBalancer(t *testing.T) { Port: 80, Protocol: api.ProtocolUDP, }}, - CreateExternalLoadBalancer: true, + Type: api.ServiceTypeLoadBalancer, }, }, expectErr: true, @@ -79,7 +79,7 @@ func TestCreateExternalLoadBalancer(t *testing.T) { Port: 80, Protocol: api.ProtocolTCP, }}, - CreateExternalLoadBalancer: true, + Type: api.ServiceTypeLoadBalancer, }, }, expectErr: false, @@ -144,15 +144,15 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) { { // Services do not have external load balancers: no calls should be made. services: []*api.Service{ - newService("s0", "111", false), - newService("s1", "222", false), + newService("s0", "111", api.ServiceTypeClusterIP), + newService("s1", "222", api.ServiceTypeClusterIP), }, expectedUpdateCalls: nil, }, { // Services does have an external load balancer: one call should be made. services: []*api.Service{ - newService("s0", "333", true), + newService("s0", "333", api.ServiceTypeLoadBalancer), }, expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{ {Name: "a333", Region: region, Hosts: []string{"node0", "node1", "node73"}}, @@ -161,9 +161,9 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) { { // Three services have an external load balancer: three calls. services: []*api.Service{ - newService("s0", "444", true), - newService("s1", "555", true), - newService("s2", "666", true), + newService("s0", "444", api.ServiceTypeLoadBalancer), + newService("s1", "555", api.ServiceTypeLoadBalancer), + newService("s2", "666", api.ServiceTypeLoadBalancer), }, expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{ {Name: "a444", Region: region, Hosts: []string{"node0", "node1", "node73"}}, @@ -174,10 +174,10 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) { { // Two services have an external load balancer and two don't: two calls. services: []*api.Service{ - newService("s0", "777", false), - newService("s1", "888", true), - newService("s3", "999", true), - newService("s4", "123", false), + newService("s0", "777", api.ServiceTypeClusterIP), + newService("s1", "888", api.ServiceTypeLoadBalancer), + newService("s3", "999", api.ServiceTypeLoadBalancer), + newService("s4", "123", api.ServiceTypeClusterIP), }, expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{ {Name: "a888", Region: region, Hosts: []string{"node0", "node1", "node73"}}, @@ -187,7 +187,7 @@ func TestUpdateNodesInExternalLoadBalancer(t *testing.T) { { // One service has an external load balancer and one is nil: one call. services: []*api.Service{ - newService("s0", "234", true), + newService("s0", "234", api.ServiceTypeLoadBalancer), nil, }, expectedUpdateCalls: []fake_cloud.FakeUpdateBalancerCall{ diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index 5da1604b913..fe9ead8550f 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -46,7 +46,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream` func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--create-external-load-balancer=bool]", + Use: "expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type]", Short: "Take a replicated application and expose it as Kubernetes Service", Long: expose_long, Example: expose_example, @@ -60,7 +60,8 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().String("protocol", "TCP", "The network protocol for the service to be created. Default is 'tcp'.") cmd.Flags().Int("port", -1, "The port that the service should serve on. Required.") cmd.MarkFlagRequired("port") - cmd.Flags().Bool("create-external-load-balancer", false, "If true, create an external load balancer for this service. Implementation is cloud provider dependent. Default is 'false'.") + cmd.Flags().String("type", "", "Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP' unless --create-external-load-balancer is specified.") + cmd.Flags().Bool("create-external-load-balancer", false, "If true, create an external load balancer for this service (trumped by --type). Implementation is cloud provider dependent. Default is 'false'.") cmd.Flags().String("selector", "", "A label selector to use for this service. If empty (the default) infer the selector from the replication controller.") cmd.Flags().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.") @@ -161,6 +162,9 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str } params["labels"] = kubectl.MakeLabels(labels) } + if v := cmdutil.GetFlagString(cmd, "type"); v != "" { + params["type"] = v + } err = kubectl.ValidateParams(names, params) if err != nil { return err diff --git a/pkg/kubectl/cmd/get_test.go b/pkg/kubectl/cmd/get_test.go index f5328687721..8ea6e6dbca0 100644 --- a/pkg/kubectl/cmd/get_test.go +++ b/pkg/kubectl/cmd/get_test.go @@ -68,6 +68,7 @@ func testData() (*api.PodList, *api.ServiceList, *api.ReplicationControllerList) ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, Spec: api.ServiceSpec{ SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, }, }, }, diff --git a/pkg/kubectl/cmd/util/helpers_test.go b/pkg/kubectl/cmd/util/helpers_test.go index f9ba4dbbfe9..d9359eace52 100644 --- a/pkg/kubectl/cmd/util/helpers_test.go +++ b/pkg/kubectl/cmd/util/helpers_test.go @@ -141,6 +141,7 @@ func TestMerge(t *testing.T) { expected: &api.Service{ Spec: api.ServiceSpec{ SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, }, }, }, @@ -157,6 +158,7 @@ func TestMerge(t *testing.T) { expected: &api.Service{ Spec: api.ServiceSpec{ SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, Selector: map[string]string{ "version": "v2", }, diff --git a/pkg/kubectl/describe.go b/pkg/kubectl/describe.go index 4805f79fbe2..127728613ea 100644 --- a/pkg/kubectl/describe.go +++ b/pkg/kubectl/describe.go @@ -505,6 +505,7 @@ func describeService(service *api.Service, endpoints *api.Endpoints, events *api fmt.Fprintf(out, "Name:\t%s\n", service.Name) fmt.Fprintf(out, "Labels:\t%s\n", formatLabels(service.Labels)) fmt.Fprintf(out, "Selector:\t%s\n", formatLabels(service.Spec.Selector)) + fmt.Fprintf(out, "Type:\t%s\n", service.Spec.Type) fmt.Fprintf(out, "IP:\t%s\n", service.Spec.PortalIP) if len(service.Status.LoadBalancer.Ingress) > 0 { list := buildIngressString(service.Status.LoadBalancer.Ingress) diff --git a/pkg/kubectl/service.go b/pkg/kubectl/service.go index c0a0ba59892..08c0becafc1 100644 --- a/pkg/kubectl/service.go +++ b/pkg/kubectl/service.go @@ -35,6 +35,7 @@ func (ServiceGenerator) ParamNames() []GeneratorParam { {"labels", false}, {"public-ip", false}, {"create-external-load-balancer", false}, + {"type", false}, {"protocol", false}, {"container-port", false}, // alias of target-port {"target-port", false}, @@ -102,10 +103,13 @@ func (ServiceGenerator) Generate(params map[string]string) (runtime.Object, erro service.Spec.Ports[0].TargetPort = util.NewIntOrStringFromInt(port) } if params["create-external-load-balancer"] == "true" { - service.Spec.CreateExternalLoadBalancer = true + service.Spec.Type = api.ServiceTypeLoadBalancer } if len(params["public-ip"]) != 0 { service.Spec.PublicIPs = []string{params["public-ip"]} } + if len(params["type"]) != 0 { + service.Spec.Type = api.ServiceType(params["type"]) + } return &service, nil } diff --git a/pkg/kubectl/service_test.go b/pkg/kubectl/service_test.go index 3e6fc976ce4..d98a40eed38 100644 --- a/pkg/kubectl/service_test.go +++ b/pkg/kubectl/service_test.go @@ -175,8 +175,8 @@ func TestGenerateService(t *testing.T) { TargetPort: util.NewIntOrStringFromString("foobar"), }, }, - PublicIPs: []string{"1.2.3.4"}, - CreateExternalLoadBalancer: true, + PublicIPs: []string{"1.2.3.4"}, + Type: api.ServiceTypeLoadBalancer, }, }, }, diff --git a/pkg/registry/etcd/etcd_test.go b/pkg/registry/etcd/etcd_test.go index 9543f517ed3..0dac8050a64 100644 --- a/pkg/registry/etcd/etcd_test.go +++ b/pkg/registry/etcd/etcd_test.go @@ -601,6 +601,7 @@ func TestEtcdUpdateService(t *testing.T) { "baz": "bar", }, SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, }, } _, err := registry.UpdateService(ctx, &testService) diff --git a/pkg/registry/service/rest_test.go b/pkg/registry/service/rest_test.go index 98529b71823..137809ef8c7 100644 --- a/pkg/registry/service/rest_test.go +++ b/pkg/registry/service/rest_test.go @@ -68,6 +68,7 @@ func TestServiceRegistryCreate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -109,6 +110,7 @@ func TestServiceStorageValidatesCreate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -120,6 +122,7 @@ func TestServiceStorageValidatesCreate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Protocol: api.ProtocolTCP, }}, @@ -162,6 +165,7 @@ func TestServiceRegistryUpdate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz2"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -205,6 +209,7 @@ func TestServiceStorageValidatesUpdate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -216,6 +221,7 @@ func TestServiceStorageValidatesUpdate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"ThisSelectorFailsValidation": "ok"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -240,9 +246,9 @@ func TestServiceRegistryExternalService(t *testing.T) { svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Selector: map[string]string{"bar": "baz"}, - CreateExternalLoadBalancer: true, - SessionAffinity: api.ServiceAffinityNone, + Selector: map[string]string{"bar": "baz"}, + SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeLoadBalancer, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -270,6 +276,7 @@ func TestServiceRegistryDelete(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -289,9 +296,9 @@ func TestServiceRegistryDeleteExternal(t *testing.T) { svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo"}, Spec: api.ServiceSpec{ - Selector: map[string]string{"bar": "baz"}, - CreateExternalLoadBalancer: true, - SessionAffinity: api.ServiceAffinityNone, + Selector: map[string]string{"bar": "baz"}, + SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeLoadBalancer, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -313,9 +320,9 @@ func TestServiceRegistryUpdateExternalService(t *testing.T) { svc1 := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: api.ServiceSpec{ - Selector: map[string]string{"bar": "baz"}, - CreateExternalLoadBalancer: false, - SessionAffinity: api.ServiceAffinityNone, + Selector: map[string]string{"bar": "baz"}, + SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -328,7 +335,7 @@ func TestServiceRegistryUpdateExternalService(t *testing.T) { // Modify load balancer to be external. svc2 := deepCloneService(svc1) - svc2.Spec.CreateExternalLoadBalancer = true + svc2.Spec.Type = api.ServiceTypeLoadBalancer if _, _, err := storage.Update(ctx, svc2); err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -349,9 +356,9 @@ func TestServiceRegistryUpdateMultiPortExternalService(t *testing.T) { svc1 := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: api.ServiceSpec{ - Selector: map[string]string{"bar": "baz"}, - CreateExternalLoadBalancer: true, - SessionAffinity: api.ServiceAffinityNone, + Selector: map[string]string{"bar": "baz"}, + SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeLoadBalancer, Ports: []api.ServicePort{{ Name: "p", Port: 6502, @@ -491,6 +498,7 @@ func TestServiceRegistryIPAllocation(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -512,6 +520,7 @@ func TestServiceRegistryIPAllocation(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -541,6 +550,7 @@ func TestServiceRegistryIPAllocation(t *testing.T) { Selector: map[string]string{"bar": "baz"}, PortalIP: testIP, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -566,6 +576,7 @@ func TestServiceRegistryIPReallocation(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -582,13 +593,17 @@ func TestServiceRegistryIPReallocation(t *testing.T) { t.Errorf("Unexpected PortalIP: %s", created_service_1.Spec.PortalIP) } - rest.Delete(ctx, created_service_1.Name) + _, err := rest.Delete(ctx, created_service_1.Name) + if err != nil { + t.Errorf("Unexpected error deleting service: %v", err) + } svc2 := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "bar"}, Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -614,6 +629,7 @@ func TestServiceRegistryIPUpdate(t *testing.T) { Spec: api.ServiceSpec{ Selector: map[string]string{"bar": "baz"}, SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -649,15 +665,15 @@ func TestServiceRegistryIPUpdate(t *testing.T) { } } -func TestServiceRegistryIPExternalLoadBalancer(t *testing.T) { +func TestServiceRegistryIPLoadBalancer(t *testing.T) { rest, _ := NewTestREST(t, nil) svc := &api.Service{ ObjectMeta: api.ObjectMeta{Name: "foo", ResourceVersion: "1"}, Spec: api.ServiceSpec{ - Selector: map[string]string{"bar": "baz"}, - CreateExternalLoadBalancer: true, - SessionAffinity: api.ServiceAffinityNone, + Selector: map[string]string{"bar": "baz"}, + SessionAffinity: api.ServiceAffinityNone, + Type: api.ServiceTypeLoadBalancer, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -730,6 +746,7 @@ func TestCreate(t *testing.T) { Selector: map[string]string{"bar": "baz"}, PortalIP: "None", SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, @@ -746,6 +763,7 @@ func TestCreate(t *testing.T) { Selector: map[string]string{"bar": "baz"}, PortalIP: "invalid", SessionAffinity: "None", + Type: api.ServiceTypeClusterIP, Ports: []api.ServicePort{{ Port: 6502, Protocol: api.ProtocolTCP, diff --git a/test/e2e/service.go b/test/e2e/service.go index 6f50b152da1..3ec0d2df08e 100644 --- a/test/e2e/service.go +++ b/test/e2e/service.go @@ -253,7 +253,7 @@ var _ = Describe("Services", func() { It("should be able to create a functioning external load balancer", func() { if !providerIs("gce", "gke") { - By(fmt.Sprintf("Skipping service external load balancer test; uses createExternalLoadBalancer, a (gce|gke) feature")) + By(fmt.Sprintf("Skipping service external load balancer test; uses ServiceTypeLoadBalancer, a (gce|gke) feature")) return } @@ -272,7 +272,7 @@ var _ = Describe("Services", func() { Port: 80, TargetPort: util.NewIntOrStringFromInt(80), }}, - CreateExternalLoadBalancer: true, + Type: api.ServiceTypeLoadBalancer, }, } @@ -353,7 +353,7 @@ var _ = Describe("Services", func() { It("should correctly serve identically named services in different namespaces on different external IP addresses", func() { if !providerIs("gce", "gke") { - By(fmt.Sprintf("Skipping service namespace collision test; uses createExternalLoadBalancer, a (gce|gke) feature")) + By(fmt.Sprintf("Skipping service namespace collision test; uses ServiceTypeLoadBalancer, a (gce|gke) feature")) return } @@ -370,7 +370,7 @@ var _ = Describe("Services", func() { Port: 80, TargetPort: util.NewIntOrStringFromInt(80), }}, - CreateExternalLoadBalancer: true, + Type: api.ServiceTypeLoadBalancer, }, }