diff --git a/docs/kubectl-expose.md b/docs/kubectl-expose.md index c803873ad84..5bbc2658bf4 100644 --- a/docs/kubectl-expose.md +++ b/docs/kubectl-expose.md @@ -7,11 +7,11 @@ Take a replicated application and expose it as Kubernetes Service Take a replicated application and expose it as Kubernetes Service. -Looks up a ReplicationController by name, and uses the selector for that replication controller -as the selector for a new Service on the specified port. +Looks up a replication controller or service by name and uses the selector for that resource as the +selector for a new Service on the specified port. ``` -kubectl expose NAME --port=port [--protocol=TCP|UDP] [--container-port=number-or-name] [--service-name=name] [--public-ip=ip] [--create-external-load-balancer=bool] +kubectl expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--container-port=number-or-name] [--service-name=name] [--public-ip=ip] [--create-external-load-balancer=bool] ``` ### Examples @@ -20,6 +20,9 @@ kubectl expose NAME --port=port [--protocol=TCP|UDP] [--container-port=number-or // Creates a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000. $ kubectl expose nginx --port=80 --container-port=8000 +// Creates a second service based on the above service, exposing the container port 8443 as port 443 with the name "nginx-https" +$ kubectl expose service nginx --port=443 --container-port=8443 --service-name=nginx-https + // Create a service for a replicated streaming application on port 4100 balancing UDP traffic and named 'video-stream'. $ kubectl expose streamer --port=4100 --protocol=udp --service-name=video-stream ``` diff --git a/docs/man/man1/kubectl-expose.1 b/docs/man/man1/kubectl-expose.1 index c1f04cfad48..977b9a9604f 100644 --- a/docs/man/man1/kubectl-expose.1 +++ b/docs/man/man1/kubectl-expose.1 @@ -16,8 +16,8 @@ kubectl expose \- Take a replicated application and expose it as Kubernetes Serv Take a replicated application and expose it as Kubernetes Service. .PP -Looks up a ReplicationController by name, and uses the selector for that replication controller -as the selector for a new Service on the specified port. +Looks up a replication controller or service by name and uses the selector for that resource as the +selector for a new Service on the specified port. .SH OPTIONS @@ -197,6 +197,9 @@ as the selector for a new Service on the specified port. // Creates a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000. $ kubectl expose nginx \-\-port=80 \-\-container\-port=8000 +// Creates a second service based on the above service, exposing the container port 8443 as port 443 with the name "nginx\-https" +$ kubectl expose service nginx \-\-port=443 \-\-container\-port=8443 \-\-service\-name=nginx\-https + // Create a service for a replicated streaming application on port 4100 balancing UDP traffic and named 'video\-stream'. $ kubectl expose streamer \-\-port=4100 \-\-protocol=udp \-\-service\-name=video\-stream diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 295e3cad777..800099640cc 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -131,11 +131,13 @@ for version in "${kube_api_versions[@]}"; do labels_field="labels" service_selector_field="selector" rc_replicas_field="desiredState.replicas" + port_field="port" if [ "$version" = "v1beta3" ]; then id_field="metadata.name" labels_field="metadata.labels" service_selector_field="spec.selector" rc_replicas_field="spec.replicas" + port_field="spec.port" fi # Passing no arguments to create is an error @@ -475,6 +477,26 @@ __EOF__ # Post-condition: 3 replicas kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '3' + ### Expose replication controller as service + # Pre-condition: 3 replicas + kube::test::get_object_assert 'rc frontend-controller' "{{.$rc_replicas_field}}" '3' + # Command + kubectl expose rc frontend-controller --port=80 "${kube_flags[@]}" + # Post-condition: service exists + kube::test::get_object_assert 'service frontend-controller' "{{.$port_field}}" '80' + # Command + kubectl expose service frontend-controller --port=443 --service-name=frontend-controller-2 "${kube_flags[@]}" + # Post-condition: service exists + kube::test::get_object_assert 'service frontend-controller-2' "{{.$port_field}}" '443' + # Command + kubectl create -f examples/limitrange/valid-pod.json "${kube_flags[@]}" + kubectl expose pod valid-pod --port=444 --service-name=frontend-controller-3 "${kube_flags[@]}" + # Post-condition: service exists + kube::test::get_object_assert 'service frontend-controller-3' "{{.$port_field}}" '444' + # Cleanup services + kubectl delete pod valid-pod "${kube_flags[@]}" + kubectl delete service frontend-controller{,-2,-3} "${kube_flags[@]}" + ### Delete replication controller with id # Pre-condition: frontend replication controller is running kube::test::get_object_assert rc "{{range.items}}{{.$id_field}}:{{end}}" 'frontend-controller:' diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 37df9313bfa..483779bacb4 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -45,6 +45,8 @@ const ( // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. // TODO: make the functions interfaces +// TODO: pass the various interfaces on the factory directly into the command constructors (so the +// commands are decoupled from the factory). type Factory struct { clients *clientCache flags *pflag.FlagSet @@ -66,6 +68,9 @@ type Factory struct { Resizer func(mapping *meta.RESTMapping) (kubectl.Resizer, error) // Returns a Reaper for gracefully shutting down resources. Reaper func(mapping *meta.RESTMapping) (kubectl.Reaper, error) + // PodSelectorForResource returns the pod selector associated with the provided resource name + // or an error. + PodSelectorForResource func(mapping *meta.RESTMapping, namespace, name string) (string, error) // Returns a schema that can validate objects stored on disk. Validator func() (validation.Schema, error) // Returns the default namespace to use in cases where no other namespace is specified @@ -128,6 +133,41 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory { Printer: func(mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return kubectl.NewHumanReadablePrinter(noHeaders), nil }, + PodSelectorForResource: func(mapping *meta.RESTMapping, namespace, name string) (string, error) { + // TODO: replace with a swagger schema based approach (identify pod selector via schema introspection) + client, err := clients.ClientForVersion("") + if err != nil { + return "", err + } + switch mapping.Kind { + case "ReplicationController": + rc, err := client.ReplicationControllers(namespace).Get(name) + if err != nil { + return "", err + } + return kubectl.MakeLabels(rc.Spec.Selector), nil + case "Pod": + rc, err := client.Pods(namespace).Get(name) + if err != nil { + return "", err + } + if len(rc.Labels) == 0 { + return "", fmt.Errorf("the pod has no labels and cannot be exposed") + } + return kubectl.MakeLabels(rc.Labels), nil + case "Service": + rc, err := client.ReplicationControllers(namespace).Get(name) + if err != nil { + return "", err + } + if rc.Spec.Selector == nil { + return "", fmt.Errorf("the service has no pod selector set") + } + return kubectl.MakeLabels(rc.Spec.Selector), nil + default: + return "", fmt.Errorf("it is not possible to get a pod selector from %s", mapping.Kind) + } + }, Resizer: func(mapping *meta.RESTMapping) (kubectl.Resizer, error) { client, err := clients.ClientForVersion(mapping.APIVersion) if err != nil { diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index 4a0429ffffa..2a9b9dc03fd 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -29,19 +29,22 @@ import ( const ( expose_long = `Take a replicated application and expose it as Kubernetes Service. -Looks up a ReplicationController by name, and uses the selector for that replication controller -as the selector for a new Service on the specified port.` +Looks up a replication controller or service by name and uses the selector for that resource as the +selector for a new Service on the specified port.` expose_example = `// Creates a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000. $ kubectl expose nginx --port=80 --container-port=8000 +// Creates a second service based on the above service, exposing the container port 8443 as port 443 with the name "nginx-https" +$ kubectl expose service nginx --port=443 --container-port=8443 --service-name=nginx-https + // Create a service for a replicated streaming application on port 4100 balancing UDP traffic and named 'video-stream'. $ kubectl expose streamer --port=4100 --protocol=udp --service-name=video-stream` ) func (f *Factory) NewCmdExposeService(out io.Writer) *cobra.Command { cmd := &cobra.Command{ - Use: "expose NAME --port=port [--protocol=TCP|UDP] [--container-port=number-or-name] [--service-name=name] [--public-ip=ip] [--create-external-load-balancer=bool]", + Use: "expose RESOURCE NAME --port=port [--protocol=TCP|UDP] [--container-port=number-or-name] [--service-name=name] [--public-ip=ip] [--create-external-load-balancer=bool]", Short: "Take a replicated application and expose it as Kubernetes Service", Long: expose_long, Example: expose_example, @@ -66,8 +69,12 @@ func (f *Factory) NewCmdExposeService(out io.Writer) *cobra.Command { } func RunExpose(f *Factory, out io.Writer, cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return util.UsageError(cmd, " is required for expose") + var name, resource string + switch l := len(args); { + case l == 2: + resource, name = args[0], args[1] + default: + return util.UsageError(cmd, "the type and name of a resource to expose are required arguments") } namespace, err := f.DefaultNamespace() @@ -83,7 +90,7 @@ func RunExpose(f *Factory, out io.Writer, cmd *cobra.Command, args []string) err generator, found := kubectl.Generators[generatorName] if !found { - return util.UsageError(cmd, fmt.Sprintf("Generator: %s not found.", generator)) + return util.UsageError(cmd, fmt.Sprintf("generator %q not found.", generator)) } if util.GetFlagInt(cmd, "port") < 1 { return util.UsageError(cmd, "--port is required and must be a positive integer.") @@ -91,16 +98,25 @@ func RunExpose(f *Factory, out io.Writer, cmd *cobra.Command, args []string) err names := generator.ParamNames() params := kubectl.MakeParams(cmd, names) if len(util.GetFlagString(cmd, "service-name")) == 0 { - params["name"] = args[0] + params["name"] = name } else { params["name"] = util.GetFlagString(cmd, "service-name") } - if _, found := params["selector"]; !found { - rc, err := client.ReplicationControllers(namespace).Get(args[0]) + if s, found := params["selector"]; !found || len(s) == 0 { + mapper, _ := f.Object() + v, k, err := mapper.VersionAndKindForResource(resource) if err != nil { return err } - params["selector"] = kubectl.MakeLabels(rc.Spec.Selector) + mapping, err := mapper.RESTMapping(k, v) + if err != nil { + return err + } + s, err := f.PodSelectorForResource(mapping, namespace, name) + if err != nil { + return err + } + params["selector"] = s } if util.GetFlagBool(cmd, "create-external-load-balancer") { params["create-external-load-balancer"] = "true"