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.
This commit is contained in:
Clayton Coleman 2015-03-25 22:35:26 -04:00
parent a34f39aee4
commit 64f91f7dac
5 changed files with 99 additions and 15 deletions

View File

@ -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
```

View File

@ -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

View File

@ -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:'

View File

@ -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 {

View File

@ -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, "<name> 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"