Merge pull request #15921 from brendandburns/fix2

Add a --expose flag to kubectl run
This commit is contained in:
Jeff Lowdermilk 2015-10-21 13:17:26 -07:00
commit d4de35e177
6 changed files with 314 additions and 56 deletions

View File

@ -747,6 +747,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=")

View File

@ -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'.
@ -78,7 +82,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
@ -92,6 +96,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.)

View File

@ -83,6 +83,7 @@ $ kubectl run nginx --image=nginx --command -- <cmd> <arg1> ... <argN>
--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.
@ -93,10 +94,12 @@ $ kubectl run nginx --image=nginx --command -- <cmd> <arg1> ... <argN>
-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.
@ -136,7 +139,7 @@ $ kubectl run nginx --image=nginx --command -- <cmd> <arg1> ... <argN>
* [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
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_run.md?pixel)]()

View File

@ -262,8 +262,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

View File

@ -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.")
@ -95,7 +102,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 {
@ -129,6 +138,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 --replicas=1, found %d", restartPolicy, replicas))
}
generatorName := cmdutil.GetFlagString(cmd, "generator")
if len(generatorName) == 0 {
if restartPolicy == api.RestartPolicyAlways {
@ -150,64 +160,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")
@ -337,3 +303,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
}

View File

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