From 54fd60727ea35913a4ae358785c5b66a2b70416e Mon Sep 17 00:00:00 2001 From: Brendan Burns Date: Tue, 20 Oct 2015 11:27:26 -0700 Subject: [PATCH] add a --expose flag to kubectl run --- contrib/completions/bash/kubectl | 3 + docs/man/man1/kubectl-run.1 | 14 +- docs/user-guide/kubectl/kubectl_run.md | 7 +- hack/verify-flags/known-flags.txt | 2 + pkg/kubectl/cmd/run.go | 179 +++++++++++++++++-------- pkg/kubectl/cmd/run_test.go | 165 +++++++++++++++++++++++ 6 files changed, 314 insertions(+), 56 deletions(-) diff --git a/contrib/completions/bash/kubectl b/contrib/completions/bash/kubectl index 2d707aebcae..a38f25db831 100644 --- a/contrib/completions/bash/kubectl +++ b/contrib/completions/bash/kubectl @@ -748,6 +748,7 @@ _kubectl_run() flags+=("--command") flags+=("--dry-run") flags+=("--env=") + flags+=("--expose") flags+=("--generator=") flags+=("--hostport=") flags+=("--image=") @@ -764,6 +765,8 @@ _kubectl_run() two_word_flags+=("-r") flags+=("--requests=") flags+=("--restart=") + flags+=("--service-generator=") + flags+=("--service-overrides=") flags+=("--show-all") flags+=("-a") flags+=("--sort-by=") diff --git a/docs/man/man1/kubectl-run.1 b/docs/man/man1/kubectl-run.1 index 033437b3f91..e49bd2beb18 100644 --- a/docs/man/man1/kubectl-run.1 +++ b/docs/man/man1/kubectl-run.1 @@ -34,6 +34,10 @@ Creates a replication controller to manage the created container(s). \fB\-\-env\fP=[] Environment variables to set in the container +.PP +\fB\-\-expose\fP=false + If true, a public, external service is created for the container(s) which are run + .PP \fB\-\-generator\fP="" The name of the API generator to use. Default is 'run/v1' if \-\-restart=Always, otherwise the default is 'run\-pod/v1'. @@ -74,7 +78,7 @@ Creates a replication controller to manage the created container(s). .PP \fB\-\-port\fP=\-1 - The port that this container exposes. + The port that this container exposes. If \-\-expose is true, this is also the port used by the service that is created. .PP \fB\-r\fP, \fB\-\-replicas\fP=1 @@ -88,6 +92,14 @@ Creates a replication controller to manage the created container(s). \fB\-\-restart\fP="Always" The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, only the Pod is created and \-\-replicas must be 1. Default 'Always' +.PP +\fB\-\-service\-generator\fP="service/v2" + The name of the generator to use for creating a service. Only used if \-\-expose is true + +.PP +\fB\-\-service\-overrides\fP="" + An inline JSON override for the generated service object. If this is non\-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. Only used if \-\-expose is true. + .PP \fB\-a\fP, \fB\-\-show\-all\fP=false When printing, show all resources (default hide terminated pods.) diff --git a/docs/user-guide/kubectl/kubectl_run.md b/docs/user-guide/kubectl/kubectl_run.md index 993b70c57bd..99f0ca59168 100644 --- a/docs/user-guide/kubectl/kubectl_run.md +++ b/docs/user-guide/kubectl/kubectl_run.md @@ -83,6 +83,7 @@ $ kubectl run nginx --image=nginx --command -- ... --command[=false]: If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default. --dry-run[=false]: If true, only print the object that would be sent, without sending it. --env=[]: Environment variables to set in the container + --expose[=false]: If true, a public, external service is created for the container(s) which are run --generator="": The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'. --hostport=-1: The host port mapping for the container port. To demonstrate a single-machine container. --image="": The image for the container to run. @@ -92,10 +93,12 @@ $ kubectl run nginx --image=nginx --command -- ... -o, --output="": Output format. One of: json|yaml|wide|name|go-template=...|go-template-file=...|jsonpath=...|jsonpath-file=... See golang template [http://golang.org/pkg/text/template/#pkg-overview] and jsonpath template [http://releases.k8s.io/HEAD/docs/user-guide/jsonpath.md]. --output-version="": Output the formatted object with the given version (default api-version). --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. - --port=-1: The port that this container exposes. + --port=-1: The port that this container exposes. If --expose is true, this is also the port used by the service that is created. -r, --replicas=1: Number of replicas to create for this container. Default is 1. --requests="": The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi' --restart="Always": The restart policy for this Pod. Legal values [Always, OnFailure, Never]. If set to 'Always' a replication controller is created for this pod, if set to OnFailure or Never, only the Pod is created and --replicas must be 1. Default 'Always' + --service-generator="service/v2": The name of the generator to use for creating a service. Only used if --expose is true + --service-overrides="": An inline JSON override for the generated service object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. Only used if --expose is true. -a, --show-all[=false]: When printing, show all resources (default hide terminated pods.) --sort-by="": If non-empty, sort list types using this field specification. The field specification is expressed as a JSONPath expression (e.g. 'ObjectMeta.Name'). The field in the API resource specified by this JSONPath expression must be an integer or a string. -i, --stdin[=false]: Keep stdin open on the container(s) in the pod, even if nothing is attached. @@ -135,7 +138,7 @@ $ kubectl run nginx --image=nginx --command -- ... * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra on 9-Oct-2015 +###### Auto generated by spf13/cobra on 20-Oct-2015 [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_run.md?pixel)]() diff --git a/hack/verify-flags/known-flags.txt b/hack/verify-flags/known-flags.txt index 5300b41d0d1..3ac9630a785 100644 --- a/hack/verify-flags/known-flags.txt +++ b/hack/verify-flags/known-flags.txt @@ -260,8 +260,10 @@ service-account-lookup service-account-private-key-file service-address service-cluster-ip-range +service-generator service-node-port-range service-node-ports +service-overrides service-sync-period session-affinity since-seconds diff --git a/pkg/kubectl/cmd/run.go b/pkg/kubectl/cmd/run.go index a4a3b018269..03a31292aeb 100644 --- a/pkg/kubectl/cmd/run.go +++ b/pkg/kubectl/cmd/run.go @@ -24,12 +24,14 @@ import ( "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/meta" client "k8s.io/kubernetes/pkg/client/unversioned" "k8s.io/kubernetes/pkg/fields" "k8s.io/kubernetes/pkg/kubectl" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/resource" "k8s.io/kubernetes/pkg/labels" + "k8s.io/kubernetes/pkg/runtime" ) const ( @@ -77,6 +79,11 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c }, } cmdutil.AddPrinterFlags(cmd) + addRunFlags(cmd) + return cmd +} + +func addRunFlags(cmd *cobra.Command) { cmd.Flags().String("generator", "", "The name of the API generator to use. Default is 'run/v1' if --restart=Always, otherwise the default is 'run-pod/v1'.") cmd.Flags().String("image", "", "The image for the container to run.") cmd.MarkFlagRequired("image") @@ -84,7 +91,7 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c cmd.Flags().Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.") 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().StringSlice("env", []string{}, "Environment variables to set in the container") - cmd.Flags().Int("port", -1, "The port that this container exposes.") + cmd.Flags().Int("port", -1, "The port that this container exposes. If --expose is true, this is also the port used by the service that is created.") cmd.Flags().Int("hostport", -1, "The host port mapping for the container port. To demonstrate a single-machine container.") cmd.Flags().StringP("labels", "l", "", "Labels to apply to the pod(s).") cmd.Flags().BoolP("stdin", "i", false, "Keep stdin open on the container(s) in the pod, even if nothing is attached.") @@ -94,7 +101,9 @@ func NewCmdRun(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer) *c cmd.Flags().Bool("command", false, "If true and extra arguments are present, use them as the 'command' field in the container, rather than the 'args' field which is the default.") cmd.Flags().String("requests", "", "The resource requirement requests for this container. For example, 'cpu=100m,memory=256Mi'") cmd.Flags().String("limits", "", "The resource requirement limits for this container. For example, 'cpu=200m,memory=512Mi'") - return cmd + cmd.Flags().Bool("expose", false, "If true, a public, external service is created for the container(s) which are run") + cmd.Flags().String("service-generator", "service/v2", "The name of the generator to use for creating a service. Only used if --expose is true") + cmd.Flags().String("service-overrides", "", "An inline JSON override for the generated service object. If this is non-empty, it is used to override the generated object. Requires that the object supply a valid apiVersion field. Only used if --expose is true.") } func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cobra.Command, args []string) error { @@ -128,6 +137,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob if restartPolicy != api.RestartPolicyAlways && replicas != 1 { return cmdutil.UsageError(cmd, fmt.Sprintf("--restart=%s requires that --repliacs=1, found %d", restartPolicy, replicas)) } + generatorName := cmdutil.GetFlagString(cmd, "generator") if len(generatorName) == 0 { if restartPolicy == api.RestartPolicyAlways { @@ -149,64 +159,20 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob params["env"] = cmdutil.GetFlagStringSlice(cmd, "env") - err = kubectl.ValidateParams(names, params) - if err != nil { - return err - } - - obj, err := generator.Generate(params) - if err != nil { - return err - } - - mapper, typer := f.Object() - version, kind, err := typer.ObjectVersionAndKind(obj) - if err != nil { - return err - } - - inline := cmdutil.GetFlagString(cmd, "overrides") - if len(inline) > 0 { - obj, err = cmdutil.Merge(obj, inline, kind) - if err != nil { + if cmdutil.GetFlagBool(cmd, "expose") { + serviceGenerator := cmdutil.GetFlagString(cmd, "service-generator") + if len(serviceGenerator) == 0 { + return cmdutil.UsageError(cmd, fmt.Sprintf("No service generator specified")) + } + if err := generateService(f, cmd, args, serviceGenerator, params, namespace, cmdOut); err != nil { return err } } - mapping, err := mapper.RESTMapping(kind, version) + obj, kind, mapper, mapping, err := createGeneratedObject(f, cmd, generator, names, params, cmdutil.GetFlagString(cmd, "overrides"), namespace) if err != nil { return err } - client, err := f.RESTClient(mapping) - if err != nil { - return err - } - - // TODO: extract this flag to a central location, when such a location exists. - if !cmdutil.GetFlagBool(cmd, "dry-run") { - resourceMapper := &resource.Mapper{ObjectTyper: typer, RESTMapper: mapper, ClientMapper: f.ClientMapperForCommand()} - info, err := resourceMapper.InfoForObject(obj) - if err != nil { - return err - } - - // Serialize the configuration into an annotation. - if err := kubectl.UpdateApplyAnnotation(info); err != nil { - return err - } - - // Serialize the object with the annotation applied. - data, err := mapping.Codec.Encode(info.Object) - if err != nil { - return err - } - - obj, err = resource.NewHelper(client, mapping).Create(namespace, false, data) - if err != nil { - return err - } - } - attachFlag := cmd.Flags().Lookup("attach") attach := cmdutil.GetFlagBool(cmd, "attach") @@ -336,3 +302,110 @@ func getRestartPolicy(cmd *cobra.Command, interactive bool) (api.RestartPolicy, return "", cmdutil.UsageError(cmd, fmt.Sprintf("invalid restart policy: %s", restart)) } } + +func generateService(f *cmdutil.Factory, cmd *cobra.Command, args []string, serviceGenerator string, paramsIn map[string]interface{}, namespace string, out io.Writer) error { + generator, found := f.Generator(serviceGenerator) + if !found { + return fmt.Errorf("missing service generator: %s", serviceGenerator) + } + names := generator.ParamNames() + + port := cmdutil.GetFlagInt(cmd, "port") + if port < 1 { + return fmt.Errorf("--port must be a positive integer when exposing a service") + } + + params := map[string]interface{}{} + for key, value := range paramsIn { + _, isString := value.(string) + if isString { + params[key] = value + } + } + + name, found := params["name"] + if !found || len(name.(string)) == 0 { + return fmt.Errorf("name is a required parameter") + } + selector, found := params["labels"] + if !found || len(selector.(string)) == 0 { + selector = fmt.Sprintf("run=%s", name.(string)) + } + params["selector"] = selector + + if defaultName, found := params["default-name"]; !found || len(defaultName.(string)) == 0 { + params["default-name"] = name + } + + obj, _, mapper, mapping, err := createGeneratedObject(f, cmd, generator, names, params, cmdutil.GetFlagString(cmd, "service-overrides"), namespace) + if err != nil { + return err + } + + if cmdutil.GetFlagString(cmd, "output") != "" { + return f.PrintObject(cmd, obj, out) + } + cmdutil.PrintSuccess(mapper, false, out, mapping.Resource, args[0], "created") + + return nil +} + +func createGeneratedObject(f *cmdutil.Factory, cmd *cobra.Command, generator kubectl.Generator, names []kubectl.GeneratorParam, params map[string]interface{}, overrides, namespace string) (runtime.Object, string, meta.RESTMapper, *meta.RESTMapping, error) { + err := kubectl.ValidateParams(names, params) + if err != nil { + return nil, "", nil, nil, err + } + + obj, err := generator.Generate(params) + if err != nil { + return nil, "", nil, nil, err + } + + mapper, typer := f.Object() + version, kind, err := typer.ObjectVersionAndKind(obj) + if err != nil { + return nil, "", nil, nil, err + } + + if len(overrides) > 0 { + obj, err = cmdutil.Merge(obj, overrides, kind) + if err != nil { + return nil, "", nil, nil, err + } + } + + mapping, err := mapper.RESTMapping(kind, version) + if err != nil { + return nil, "", nil, nil, err + } + client, err := f.RESTClient(mapping) + if err != nil { + return nil, "", nil, nil, err + } + + // TODO: extract this flag to a central location, when such a location exists. + if !cmdutil.GetFlagBool(cmd, "dry-run") { + resourceMapper := &resource.Mapper{ObjectTyper: typer, RESTMapper: mapper, ClientMapper: f.ClientMapperForCommand()} + info, err := resourceMapper.InfoForObject(obj) + if err != nil { + return nil, "", nil, nil, err + } + + // Serialize the configuration into an annotation. + if err := kubectl.UpdateApplyAnnotation(info); err != nil { + return nil, "", nil, nil, err + } + + // Serialize the object with the annotation applied. + data, err := mapping.Codec.Encode(info.Object) + if err != nil { + return nil, "", nil, nil, err + } + + obj, err = resource.NewHelper(client, mapping).Create(namespace, false, data) + if err != nil { + return nil, "", nil, nil, err + } + } + return obj, kind, mapper, mapping, err +} diff --git a/pkg/kubectl/cmd/run_test.go b/pkg/kubectl/cmd/run_test.go index c40ab85ea11..422da9eb708 100644 --- a/pkg/kubectl/cmd/run_test.go +++ b/pkg/kubectl/cmd/run_test.go @@ -17,12 +17,20 @@ limitations under the License. package cmd import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" "reflect" "testing" "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/testapi" + client "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/client/unversioned/fake" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/util" ) func TestGetRestartPolicy(t *testing.T) { @@ -97,3 +105,160 @@ func TestGetEnv(t *testing.T) { t.Errorf("expected: %s, saw: %s", test.expected, envStrings) } } + +func TestGenerateService(t *testing.T) { + + tests := []struct { + port string + args []string + serviceGenerator string + params map[string]interface{} + expectErr bool + name string + service api.Service + expectPOST bool + }{ + { + port: "80", + args: []string{"foo"}, + serviceGenerator: "service/v2", + params: map[string]interface{}{ + "name": "foo", + }, + expectErr: false, + name: "basic", + service: api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + }, + Spec: api.ServiceSpec{ + Ports: []api.ServicePort{ + { + Port: 80, + Protocol: "TCP", + TargetPort: util.NewIntOrStringFromInt(80), + }, + }, + Selector: map[string]string{ + "run": "foo", + }, + Type: api.ServiceTypeClusterIP, + SessionAffinity: api.ServiceAffinityNone, + }, + }, + expectPOST: true, + }, + { + port: "80", + args: []string{"foo"}, + serviceGenerator: "service/v2", + params: map[string]interface{}{ + "name": "foo", + "labels": "app=bar", + }, + expectErr: false, + name: "custom labels", + service: api.Service{ + ObjectMeta: api.ObjectMeta{ + Name: "foo", + Labels: map[string]string{"app": "bar"}, + }, + Spec: api.ServiceSpec{ + Ports: []api.ServicePort{ + { + Port: 80, + Protocol: "TCP", + TargetPort: util.NewIntOrStringFromInt(80), + }, + }, + Selector: map[string]string{ + "app": "bar", + }, + Type: api.ServiceTypeClusterIP, + SessionAffinity: api.ServiceAffinityNone, + }, + }, + expectPOST: true, + }, + { + expectErr: true, + name: "missing port", + expectPOST: false, + }, + { + port: "80", + args: []string{"foo"}, + serviceGenerator: "service/v2", + params: map[string]interface{}{ + "name": "foo", + }, + expectErr: false, + name: "dry-run", + expectPOST: false, + }, + } + for _, test := range tests { + sawPOST := false + f, tf, codec := NewAPIFactory() + tf.ClientConfig = &client.Config{Version: testapi.Default.Version()} + tf.Client = &fake.RESTClient{ + Codec: codec, + Client: fake.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { + switch p, m := req.URL.Path, req.Method; { + case test.expectPOST && m == "POST" && p == "/namespaces/namespace/services": + sawPOST = true + body := objBody(codec, &test.service) + data, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + defer req.Body.Close() + svc := &api.Service{} + if err := codec.DecodeInto(data, svc); err != nil { + t.Errorf("unexpected error: %v", err) + t.FailNow() + } + // Copy things that are defaulted by the system + test.service.Annotations = svc.Annotations + + if !reflect.DeepEqual(&test.service, svc) { + t.Errorf("expected:\n%v\nsaw:\n%v\n", &test.service, svc) + } + return &http.Response{StatusCode: 200, Body: body}, nil + default: + // Ensures no GET is performed when deleting by name + t.Errorf("%s: unexpected request: %s %#v\n%#v", test.name, req.Method, req.URL, req) + return nil, fmt.Errorf("unexpected request") + } + }), + } + cmd := &cobra.Command{} + cmd.Flags().String("output", "", "") + addRunFlags(cmd) + + if !test.expectPOST { + cmd.Flags().Set("dry-run", "true") + } + + if len(test.port) > 0 { + cmd.Flags().Set("port", test.port) + test.params["port"] = test.port + } + + buff := &bytes.Buffer{} + err := generateService(f, cmd, test.args, test.serviceGenerator, test.params, "namespace", buff) + if test.expectErr { + if err == nil { + t.Error("unexpected non-error") + } + continue + } + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if test.expectPOST != sawPOST { + t.Error("expectPost: %v, sawPost: %v", test.expectPOST, sawPOST) + } + } +}