From 64f91f7daccb76d7e4ecf5956e56e8ffcae67e26 Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Wed, 25 Mar 2015 22:35:26 -0400 Subject: [PATCH] Allow `kubectl expose` to be polymorphic to the source of the selector Allows exposing new resource types to be exposed (OpenShift deployment controllers, copying services, and other resources that will have pod label selectors). Also fixed a bug where the selector wasn't exposed because of parameter defaulting. --- docs/kubectl-expose.md | 9 +++++--- docs/man/man1/kubectl-expose.1 | 7 ++++-- hack/test-cmd.sh | 22 +++++++++++++++++++ pkg/kubectl/cmd/cmd.go | 40 ++++++++++++++++++++++++++++++++++ pkg/kubectl/cmd/expose.go | 36 +++++++++++++++++++++--------- 5 files changed, 99 insertions(+), 15 deletions(-) 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 0126a4f91ca..2e6602e1b40 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 @@ -473,6 +475,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 3ac3cd6059c..cc3c411d160 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"