Merge pull request #15061 from kargakis/fix-dry-run-in-expose

Fix dry run in expose
This commit is contained in:
Dawn Chen 2015-10-12 12:53:04 -07:00
commit 075c3a2c71
5 changed files with 87 additions and 146 deletions

View File

@ -77,7 +77,7 @@ re\-use the labels from the resource it exposes.
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. 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.
.PP .PP
\fB\-\-port\fP=\-1 \fB\-\-port\fP=""
The port that the service should serve on. Copied from the resource being exposed, if unspecified The port that the service should serve on. Copied from the resource being exposed, if unspecified
.PP .PP

View File

@ -82,7 +82,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream
-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]. -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). --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. --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 the service should serve on. Copied from the resource being exposed, if unspecified --port="": The port that the service should serve on. Copied from the resource being exposed, if unspecified
--protocol="TCP": The network protocol for the service to be created. Default is 'tcp'. --protocol="TCP": The network protocol for the service to be created. Default is 'tcp'.
--selector="": A label selector to use for this service. If empty (the default) infer the selector from the replication controller. --selector="": A label selector to use for this service. If empty (the default) infer the selector from the replication controller.
--session-affinity="": If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP' --session-affinity="": If non-empty, set the session affinity for the service to this; legal values: 'None', 'ClientIP'
@ -125,7 +125,7 @@ $ kubectl expose rc streamer --port=4100 --protocol=udp --name=video-stream
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager
###### Auto generated by spf13/cobra at 2015-09-22 12:53:42.292344962 +0000 UTC ###### Auto generated by spf13/cobra at 2015-10-10 14:16:24.22183637 +0000 UTC
<!-- BEGIN MUNGE: GENERATED_ANALYTICS --> <!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_expose.md?pixel)]() [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_expose.md?pixel)]()

View File

@ -228,12 +228,6 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) {
ClientConfig: func() (*client.Config, error) { ClientConfig: func() (*client.Config, error) {
return t.ClientConfig, t.Err return t.ClientConfig, t.Err
}, },
CanBeExposed: func(kind string) error {
if kind != "ReplicationController" && kind != "Service" && kind != "Pod" {
return fmt.Errorf("invalid resource provided: %v, only a replication controller, service or pod is accepted", kind)
}
return nil
},
Generator: func(name string) (kubectl.Generator, bool) { Generator: func(name string) (kubectl.Generator, bool) {
generator, ok := generators[name] generator, ok := generators[name]
return generator, ok return generator, ok
@ -241,6 +235,7 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) {
} }
rf := cmdutil.NewFactory(nil) rf := cmdutil.NewFactory(nil)
f.PodSelectorForObject = rf.PodSelectorForObject f.PodSelectorForObject = rf.PodSelectorForObject
f.CanBeExposed = rf.CanBeExposed
return f, t, testapi.Default.Codec() return f, t, testapi.Default.Codec()
} }

View File

@ -73,7 +73,7 @@ func NewCmdExposeService(f *cmdutil.Factory, out io.Writer) *cobra.Command {
cmdutil.AddPrinterFlags(cmd) cmdutil.AddPrinterFlags(cmd)
cmd.Flags().String("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'.") cmd.Flags().String("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'.")
cmd.Flags().String("protocol", "TCP", "The network protocol for the service to be created. Default is 'tcp'.") cmd.Flags().String("protocol", "TCP", "The network protocol for the service to be created. Default is 'tcp'.")
cmd.Flags().Int("port", -1, "The port that the service should serve on. Copied from the resource being exposed, if unspecified") cmd.Flags().String("port", "", "The port that the service should serve on. Copied from the resource being exposed, if unspecified")
cmd.Flags().String("type", "", "Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP'.") cmd.Flags().String("type", "", "Type for this service: ClusterIP, NodePort, or LoadBalancer. Default is 'ClusterIP'.")
// TODO: remove create-external-load-balancer in code on or after Aug 25, 2016. // TODO: remove create-external-load-balancer in code on or after Aug 25, 2016.
cmd.Flags().Bool("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'.") cmd.Flags().Bool("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'.")
@ -150,32 +150,22 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str
params["selector"] = s params["selector"] = s
} }
if cmdutil.GetFlagInt(cmd, "port") < 1 { // For objects that need a port, derive it from the exposed object in case a user
noPorts := true // didn't explicitly specify one via --port
for _, param := range names { if port, found := params["port"]; found && kubectl.IsZero(port) {
if param.Name == "port" { ports, err := f.PortsForObject(inputObject)
noPorts = false if err != nil {
break return cmdutil.UsageError(cmd, fmt.Sprintf("couldn't find port via --port flag or introspection: %s", err))
}
} }
if cmdutil.GetFlagInt(cmd, "port") < 0 && !noPorts { switch len(ports) {
ports, err := f.PortsForObject(inputObject) case 0:
if err != nil { return cmdutil.UsageError(cmd, "couldn't find port via --port flag or introspection")
return cmdutil.UsageError(cmd, fmt.Sprintf("couldn't find port via --port flag or introspection: %s", err)) case 1:
} params["port"] = ports[0]
switch len(ports) { default:
case 0: return cmdutil.UsageError(cmd, fmt.Sprintf("multiple ports to choose from: %v, please explicitly specify a port using the --port flag.", ports))
return cmdutil.UsageError(cmd, "couldn't find port via --port flag or introspection")
case 1:
params["port"] = ports[0]
default:
return cmdutil.UsageError(cmd, fmt.Sprintf("multiple ports to choose from: %v, please explicitly specify a port using the --port flag.", ports))
}
} }
} }
if cmdutil.GetFlagBool(cmd, "create-external-load-balancer") {
params["create-external-load-balancer"] = "true"
}
if kubectl.IsZero(params["labels"]) { if kubectl.IsZero(params["labels"]) {
labels, err := f.LabelsForObject(inputObject) labels, err := f.LabelsForObject(inputObject)
if err != nil { if err != nil {
@ -183,22 +173,17 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str
} }
params["labels"] = kubectl.MakeLabels(labels) params["labels"] = kubectl.MakeLabels(labels)
} }
if v := cmdutil.GetFlagString(cmd, "type"); v != "" { if err = kubectl.ValidateParams(names, params); err != nil {
params["type"] = v
}
err = kubectl.ValidateParams(names, params)
if err != nil {
return err return err
} }
// Expose new object // Generate new object
object, err := generator.Generate(params) object, err := generator.Generate(params)
if err != nil { if err != nil {
return err return err
} }
inline := cmdutil.GetFlagString(cmd, "overrides") if inline := cmdutil.GetFlagString(cmd, "overrides"); len(inline) > 0 {
if len(inline) > 0 {
object, err = cmdutil.Merge(object, inline, mapping.Kind) object, err = cmdutil.Merge(object, inline, mapping.Kind)
if err != nil { if err != nil {
return err return err
@ -212,26 +197,24 @@ func RunExpose(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []str
} }
// TODO: extract this flag to a central location, when such a location exists. // TODO: extract this flag to a central location, when such a location exists.
if cmdutil.GetFlagBool(cmd, "dry-run") { if cmdutil.GetFlagBool(cmd, "dry-run") {
fmt.Fprintln(out, "running in dry-run mode...") return f.PrintObject(cmd, object, out)
} else {
// Serialize the configuration into an annotation.
if err := kubectl.UpdateApplyAnnotation(info); err != nil {
return err
}
// Serialize the object with the annotation applied.
data, err := info.Mapping.Codec.Encode(info.Object)
if err != nil {
return err
}
object, err = resource.NewHelper(info.Client, info.Mapping).Create(namespace, false, data)
if err != nil {
return err
}
} }
outputFormat := cmdutil.GetFlagString(cmd, "output") // Serialize the configuration into an annotation.
if outputFormat != "" { if err := kubectl.UpdateApplyAnnotation(info); err != nil {
return err
}
// Serialize the object with the annotation applied.
data, err := info.Mapping.Codec.Encode(object)
if err != nil {
return err
}
object, err = resource.NewHelper(info.Client, info.Mapping).Create(namespace, false, data)
if err != nil {
return err
}
if len(cmdutil.GetFlagString(cmd, "output")) > 0 {
return f.PrintObject(cmd, object, out) return f.PrintObject(cmd, object, out)
} }
cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "exposed") cmdutil.PrintSuccess(mapper, false, out, info.Mapping.Resource, info.Name, "exposed")

View File

@ -23,8 +23,8 @@ import (
"testing" "testing"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/fake" "k8s.io/kubernetes/pkg/client/unversioned/fake"
"k8s.io/kubernetes/pkg/kubectl"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util" "k8s.io/kubernetes/pkg/util"
) )
@ -149,7 +149,7 @@ func TestRunExposeService(t *testing.T) {
Selector: map[string]string{"app": "go"}, Selector: map[string]string{"app": "go"},
}, },
}, },
flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "create-external-load-balancer": "true"}, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "type": "LoadBalancer", "dry-run": "true"},
output: &api.Service{ output: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
@ -164,8 +164,7 @@ func TestRunExposeService(t *testing.T) {
Type: api.ServiceTypeLoadBalancer, Type: api.ServiceTypeLoadBalancer,
}, },
}, },
expected: "service \"foo\" exposed", status: 200,
status: 200,
}, },
{ {
name: "expose-affinity-service", name: "expose-affinity-service",
@ -181,7 +180,7 @@ func TestRunExposeService(t *testing.T) {
Selector: map[string]string{"app": "go"}, Selector: map[string]string{"app": "go"},
}, },
}, },
flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "create-external-load-balancer": "true", "session-affinity": "ClientIP"}, flags: map[string]string{"selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "type": "LoadBalancer", "session-affinity": "ClientIP", "dry-run": "true"},
output: &api.Service{ output: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}}, ObjectMeta: api.ObjectMeta{Name: "foo", Namespace: "", Labels: map[string]string{"svc": "test"}},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
@ -197,8 +196,7 @@ func TestRunExposeService(t *testing.T) {
SessionAffinity: api.ServiceAffinityClientIP, SessionAffinity: api.ServiceAffinityClientIP,
}, },
}, },
expected: "service \"foo\" exposed", status: 200,
status: 200,
}, },
{ {
name: "expose-external-service", name: "expose-external-service",
@ -215,7 +213,7 @@ func TestRunExposeService(t *testing.T) {
}, },
}, },
// Even if we specify --selector, since service/test doesn't need one it will ignore it // Even if we specify --selector, since service/test doesn't need one it will ignore it
flags: map[string]string{"selector": "svc=fromexternal", "port": "90", "labels": "svc=fromexternal", "name": "frombaz", "generator": "service/test"}, flags: map[string]string{"selector": "svc=fromexternal", "port": "90", "labels": "svc=fromexternal", "name": "frombaz", "generator": "service/test", "dry-run": "true"},
output: &api.Service{ output: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "frombaz", Namespace: "", Labels: map[string]string{"svc": "fromexternal"}}, ObjectMeta: api.ObjectMeta{Name: "frombaz", Namespace: "", Labels: map[string]string{"svc": "fromexternal"}},
Spec: api.ServiceSpec{ Spec: api.ServiceSpec{
@ -226,11 +224,39 @@ func TestRunExposeService(t *testing.T) {
TargetPort: util.NewIntOrStringFromInt(90), TargetPort: util.NewIntOrStringFromInt(90),
}, },
}, },
Selector: map[string]string{"svc": "fromexternal"},
}, },
}, },
expected: "service \"frombaz\" exposed", status: 200,
status: 200, },
{
name: "expose-from-file",
args: []string{},
ns: "test",
calls: map[string]string{
"GET": "/namespaces/test/services/redis-master",
"POST": "/namespaces/test/services",
},
input: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "redis-master", Namespace: "test", ResourceVersion: "12"},
Spec: api.ServiceSpec{
Selector: map[string]string{"app": "go"},
},
},
flags: map[string]string{"filename": "../../../examples/guestbook/redis-master-service.yaml", "selector": "func=stream", "protocol": "UDP", "port": "14", "name": "foo", "labels": "svc=test", "dry-run": "true"},
output: &api.Service{
ObjectMeta: api.ObjectMeta{Name: "foo", Labels: map[string]string{"svc": "test"}},
Spec: api.ServiceSpec{
Ports: []api.ServicePort{
{
Protocol: api.ProtocolUDP,
Port: 14,
TargetPort: util.NewIntOrStringFromInt(14),
},
},
Selector: map[string]string{"func": "stream"},
},
},
status: 200,
}, },
{ {
name: "truncate-name", name: "truncate-name",
@ -264,7 +290,7 @@ func TestRunExposeService(t *testing.T) {
for _, test := range tests { for _, test := range tests {
f, tf, codec := NewAPIFactory() f, tf, codec := NewAPIFactory()
tf.Printer = &testPrinter{} tf.Printer = &kubectl.JSONPrinter{}
tf.Client = &fake.RESTClient{ tf.Client = &fake.RESTClient{
Codec: codec, Codec: codec,
Client: fake.HTTPClientFunc(func(req *http.Request) (*http.Response, error) { Client: fake.HTTPClientFunc(func(req *http.Request) (*http.Response, error) {
@ -289,82 +315,19 @@ func TestRunExposeService(t *testing.T) {
} }
cmd.Run(cmd, test.args) cmd.Run(cmd, test.args)
out, expectedOut := buf.String(), test.expected
if !strings.Contains(out, expectedOut) {
t.Errorf("%s: Unexpected output! Expected\n%s\ngot\n%s", test.name, expectedOut, out)
}
}
}
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: unversioned.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: unversioned.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 = &fake.RESTClient{
Codec: codec,
Client: fake.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() out := buf.String()
if _, ok := test.flags["dry-run"]; ok {
buf.Reset()
if err := tf.Printer.PrintObj(test.output, buf); err != nil {
t.Errorf("%s: Unexpected error: %v", test.name, err)
continue
}
test.expected = buf.String()
}
if !strings.Contains(out, test.expected) { if !strings.Contains(out, test.expected) {
t.Errorf("unexpected output: %s", out) t.Errorf("%s: Unexpected output! Expected\n%s\ngot\n%s", test.name, test.expected, out)
} }
} }
} }