diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index b56e201c069..c119871f0d5 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -177,6 +177,8 @@ func RunExpose(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri params["selector"] = s } + isHeadlessService := params["cluster-ip"] == "None" + // For objects that need a port, derive it from the exposed object in case a user // didn't explicitly specify one via --port if port, found := params["port"]; found && kubectl.IsZero(port) { @@ -186,7 +188,9 @@ func RunExpose(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []stri } switch len(ports) { case 0: - return cmdutil.UsageError(cmd, "couldn't find port via --port flag or introspection") + if !isHeadlessService { + return cmdutil.UsageError(cmd, "couldn't find port via --port flag or introspection") + } case 1: params["port"] = ports[0] default: diff --git a/pkg/kubectl/cmd/expose_test.go b/pkg/kubectl/cmd/expose_test.go index 0a8c6f32594..e3f25d0c748 100644 --- a/pkg/kubectl/cmd/expose_test.go +++ b/pkg/kubectl/cmd/expose_test.go @@ -264,6 +264,32 @@ func TestRunExposeService(t *testing.T) { expected: "service \"foo\" exposed", status: 200, }, + { + name: "expose-headless-service-no-port", + args: []string{"service", "baz"}, + ns: "test", + calls: map[string]string{ + "GET": "/namespaces/test/services/baz", + "POST": "/namespaces/test/services", + }, + input: &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + Spec: api.ServiceSpec{ + Selector: map[string]string{"app": "go"}, + }, + }, + flags: map[string]string{"selector": "func=stream", "name": "foo", "labels": "svc=test", "cluster-ip": "None", "dry-run": "true"}, + output: &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, + Spec: api.ServiceSpec{ + Ports: []api.ServicePort{}, + Selector: map[string]string{"func": "stream"}, + ClusterIP: api.ClusterIPNone, + }, + }, + expected: "service \"foo\" exposed", + status: 200, + }, { name: "expose-from-file", args: []string{}, diff --git a/pkg/kubectl/service.go b/pkg/kubectl/service.go index 604dbf327e0..67cf68105c3 100644 --- a/pkg/kubectl/service.go +++ b/pkg/kubectl/service.go @@ -110,6 +110,9 @@ func generate(genericParams map[string]interface{}) (runtime.Object, error) { return nil, fmt.Errorf("'name' is a required parameter.") } } + + isHeadlessService := params["cluster-ip"] == "None" + ports := []api.ServicePort{} servicePortName, found := params["port-name"] if !found { @@ -131,44 +134,46 @@ func generate(genericParams map[string]interface{}) (runtime.Object, error) { var portString string if portString, found = params["ports"]; !found { portString, found = params["port"] - if !found { + if !found && !isHeadlessService { return nil, fmt.Errorf("'port' is a required parameter.") } } - portStringSlice := strings.Split(portString, ",") - for i, stillPortString := range portStringSlice { - port, err := strconv.Atoi(stillPortString) - if err != nil { - return nil, err - } - name := servicePortName - // If we are going to assign multiple ports to a service, we need to - // generate a different name for each one. - if len(portStringSlice) > 1 { - name = fmt.Sprintf("port-%d", i+1) - } - protocol := params["protocol"] - - switch { - case len(protocol) == 0 && len(portProtocolMap) == 0: - // Default to TCP, what the flag was doing previously. - protocol = "TCP" - case len(protocol) > 0 && len(portProtocolMap) > 0: - // User has specified the --protocol while exposing a multiprotocol resource - // We should stomp multiple protocols with the one specified ie. do nothing - case len(protocol) == 0 && len(portProtocolMap) > 0: - // no --protocol and we expose a multiprotocol resource - protocol = "TCP" // have the default so we can stay sane - if exposeProtocol, found := portProtocolMap[stillPortString]; found { - protocol = exposeProtocol + if portString != "" { + portStringSlice := strings.Split(portString, ",") + for i, stillPortString := range portStringSlice { + port, err := strconv.Atoi(stillPortString) + if err != nil { + return nil, err } + name := servicePortName + // If we are going to assign multiple ports to a service, we need to + // generate a different name for each one. + if len(portStringSlice) > 1 { + name = fmt.Sprintf("port-%d", i+1) + } + protocol := params["protocol"] + + switch { + case len(protocol) == 0 && len(portProtocolMap) == 0: + // Default to TCP, what the flag was doing previously. + protocol = "TCP" + case len(protocol) > 0 && len(portProtocolMap) > 0: + // User has specified the --protocol while exposing a multiprotocol resource + // We should stomp multiple protocols with the one specified ie. do nothing + case len(protocol) == 0 && len(portProtocolMap) > 0: + // no --protocol and we expose a multiprotocol resource + protocol = "TCP" // have the default so we can stay sane + if exposeProtocol, found := portProtocolMap[stillPortString]; found { + protocol = exposeProtocol + } + } + ports = append(ports, api.ServicePort{ + Name: name, + Port: int32(port), + Protocol: api.Protocol(protocol), + }) } - ports = append(ports, api.ServicePort{ - Name: name, - Port: int32(port), - Protocol: api.Protocol(protocol), - }) } service := api.Service{ diff --git a/pkg/kubectl/service_test.go b/pkg/kubectl/service_test.go index 6d7ad342f8d..d887cf8bdc0 100644 --- a/pkg/kubectl/service_test.go +++ b/pkg/kubectl/service_test.go @@ -536,6 +536,29 @@ func TestGenerateService(t *testing.T) { }, }, }, + { + generator: ServiceGeneratorV2{}, + params: map[string]interface{}{ + "selector": "foo=bar,baz=blah", + "name": "test", + "protocol": "TCP", + "container-port": "1234", + "cluster-ip": "None", + }, + expected: api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "test", + }, + Spec: api.ServiceSpec{ + Selector: map[string]string{ + "foo": "bar", + "baz": "blah", + }, + Ports: []api.ServicePort{}, + ClusterIP: api.ClusterIPNone, + }, + }, + }, } for _, test := range tests { obj, err := test.generator.Generate(test.params)