diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 49868248234..4ad943aad67 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -700,6 +700,12 @@ _kubectl_expose() flags+=("--container-port=") flags+=("--create-external-load-balancer") flags+=("--dry-run") + flags+=("--filename=") + flags_with_completion+=("--filename") + flags_completion+=("__handle_filename_extension_flag json|yaml|yml") + two_word_flags+=("-f") + flags_with_completion+=("-f") + flags_completion+=("__handle_filename_extension_flag json|yaml|yml") flags+=("--generator=") flags+=("--help") flags+=("-h") diff --git a/docs/man/man1/kubectl-expose.1 b/docs/man/man1/kubectl-expose.1 index 8e51c69976e..50927d0cd20 100644 --- a/docs/man/man1/kubectl-expose.1 +++ b/docs/man/man1/kubectl-expose.1 @@ -34,6 +34,10 @@ re\-use the labels from the resource it exposes. \fB\-\-dry\-run\fP=false If true, only print the object that would be sent, without creating it. +.PP +\fB\-f\fP, \fB\-\-filename\fP=[] + Filename, directory, or URL to a file identifying the resource to expose a service + .PP \fB\-\-generator\fP="service/v2" The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'. @@ -206,6 +210,9 @@ re\-use the labels from the resource it exposes. // Creates a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000. $ kubectl expose rc nginx \-\-port=80 \-\-target\-port=8000 +# Creates a service for a replication controller identified by type and name specified in "nginx\-controller.yaml", which serves on port 80 and connects to the containers on port 8000. +$ kubectl expose \-f nginx\-controller.yaml \-\-port=80 \-\-target\-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 \-\-target\-port=8443 \-\-name=nginx\-https diff --git a/docs/user-guide/kubectl/kubectl_expose.md b/docs/user-guide/kubectl/kubectl_expose.md index 3e4f40e2b21..0f03eba5861 100644 --- a/docs/user-guide/kubectl/kubectl_expose.md +++ b/docs/user-guide/kubectl/kubectl_expose.md @@ -45,7 +45,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 TYPE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type] +kubectl expose (-f FILENAME | TYPE NAME) --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type] ``` ### Examples @@ -54,6 +54,9 @@ kubectl expose TYPE NAME --port=port [--protocol=TCP|UDP] [--target-port=number- // Creates a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000. $ kubectl expose rc nginx --port=80 --target-port=8000 +# Creates a service for a replication controller identified by type and name specified in "nginx-controller.yaml", which serves on port 80 and connects to the containers on port 8000. +$ kubectl expose -f nginx-controller.yaml --port=80 --target-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 --target-port=8443 --name=nginx-https @@ -67,6 +70,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 (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. + -f, --filename=[]: Filename, directory, or URL to a file identifying the resource to expose a service --generator="service/v2": The name of the API generator to use. There are 2 generators: 'service/v1' and 'service/v2'. The only difference between them is that service port in v1 is named 'default', while it is left unnamed in v2. Default is 'service/v2'. -h, --help[=false]: help for expose -l, --labels="": Labels to apply to the service created by this call. @@ -118,7 +122,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-08-07 19:25:02.004335817 +0000 UTC +###### Auto generated by spf13/cobra at 2015-08-12 08:16:20.804863501 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_expose.md?pixel)]() diff --git a/pkg/kubectl/cmd/expose.go b/pkg/kubectl/cmd/expose.go index c8be682ec8a..3e0cd359040 100644 --- a/pkg/kubectl/cmd/expose.go +++ b/pkg/kubectl/cmd/expose.go @@ -20,11 +20,11 @@ import ( "fmt" "io" + "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" - - "github.com/spf13/cobra" ) const ( @@ -37,6 +37,9 @@ re-use the labels from the resource it exposes.` expose_example = `// Creates a service for a replicated nginx, which serves on port 80 and connects to the containers on port 8000. $ kubectl expose rc nginx --port=80 --target-port=8000 +# Creates a service for a replication controller identified by type and name specified in "nginx-controller.yaml", which serves on port 80 and connects to the containers on port 8000. +$ kubectl expose -f nginx-controller.yaml --port=80 --target-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 --target-port=8443 --name=nginx-https @@ -46,7 +49,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 TYPE NAME --port=port [--protocol=TCP|UDP] [--target-port=number-or-name] [--name=name] [--public-ip=ip] [--type=type]", + Use: "expose (-f FILENAME | TYPE 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, @@ -71,11 +74,14 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command { cmd.Flags().String("overrides", "", "An inline JSON override for the generated object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field.") cmd.Flags().String("name", "", "The name for the newly created object.") cmd.Flags().String("session-affinity", "", "If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'") + + usage := "Filename, directory, or URL to a file identifying the resource to expose a service" + kubectl.AddJsonFilenameFlag(cmd, usage) return cmd } func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error { - namespace, _, err := f.DefaultNamespace() + namespace, enforceNamespace, err := f.DefaultNamespace() if err != nil { return err } @@ -84,6 +90,7 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str r := resource.NewBuilder(mapper, typer, f.ClientMapperForCommand()). ContinueOnError(). NamespaceParam(namespace).DefaultNamespace(). + FilenameParam(enforceNamespace, cmdutil.GetFlagStringSlice(cmd, "filename")...). ResourceTypeOrNameArgs(false, args...). Flatten(). Do() @@ -91,10 +98,6 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str if err != nil { return err } - mapping, err := r.ResourceMapping() - if err != nil { - return err - } infos, err := r.Infos() if err != nil { return err @@ -103,6 +106,7 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str return fmt.Errorf("multiple resources provided: %v", args) } info := infos[0] + mapping := info.ResourceMapping() // Get the input object client, err := f.RESTClient(mapping) @@ -201,6 +205,5 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str return err } } - return f.PrintObject(cmd, object, out) } diff --git a/pkg/kubectl/cmd/expose_test.go b/pkg/kubectl/cmd/expose_test.go index 5e5db53ce58..c92198aaeb7 100644 --- a/pkg/kubectl/cmd/expose_test.go +++ b/pkg/kubectl/cmd/expose_test.go @@ -202,7 +202,6 @@ func TestRunExposeService(t *testing.T) { cmd.Flags().Set(flag, value) } cmd.Run(cmd, test.args) - if len(test.expected) > 0 { out := buf.String() if !strings.Contains(out, test.expected) { @@ -211,3 +210,76 @@ func TestRunExposeService(t *testing.T) { } } } + +func TestRunExposeServiceFromFile(t *testing.T) { + test := struct { + calls map[string]string + input runtime.Object + flags map[string]string + output runtime.Object + expected string + status int + }{ + calls: map[string]string{ + "GET": "/namespaces/test/services/redis-master", + "POST": "/namespaces/test/services", + }, + input: &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "baz", Namespace: "test", ResourceVersion: "12"}, + TypeMeta: api.TypeMeta{Kind: "Service", APIVersion: "v1"}, + Spec: api.ServiceSpec{ + Selector: map[string]string{"app": "go"}, + }, + }, + flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test"}, + output: &api.Service{ + ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "test", ResourceVersion: "12", Labels: map[string]string{"svc": "test"}}, + TypeMeta: api.TypeMeta{Kind: "Service", APIVersion: "v1"}, + Spec: api.ServiceSpec{ + Ports: []api.ServicePort{ + { + Name: "default", + Protocol: api.Protocol("UDP"), + Port: 14, + }, + }, + Selector: map[string]string{"func": "stream"}, + }, + }, + status: 200, + } + + f, tf, codec := NewAPIFactory() + tf.Printer = &testPrinter{} + tf.Client = &client.FakeRESTClient{ + Codec: codec, + Client: client.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case p == test.calls[m] && m == "GET": + return &http.Response{StatusCode: test.status, Body: objBody(codec, test.input)}, nil + case p == test.calls[m] && m == "POST": + return &http.Response{StatusCode: test.status, Body: objBody(codec, test.output)}, nil + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }), + } + tf.Namespace = "test" + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdExposeService(f, buf) + cmd.SetOutput(buf) + + for flag, value := range test.flags { + cmd.Flags().Set(flag, value) + } + cmd.Flags().Set("filename", "../../../examples/guestbook/redis-master-service.yaml") + cmd.Run(cmd, []string{}) + if len(test.expected) > 0 { + out := buf.String() + if !strings.Contains(out, test.expected) { + t.Errorf("unexpected output: %s", out) + } + } +}