diff --git a/pkg/kubectl/cmd/logs.go b/pkg/kubectl/cmd/logs.go index f3fa84b9dce..1c4ac682541 100644 --- a/pkg/kubectl/cmd/logs.go +++ b/pkg/kubectl/cmd/logs.go @@ -54,13 +54,19 @@ var ( kubectl logs --tail=20 nginx # Show all logs from pod nginx written in the last hour - kubectl logs --since=1h nginx`) + kubectl logs --since=1h nginx + + # Return snapshot logs from first container of a job named hello + kubectl logs job/hello + + # Return snapshot logs from container nginx-1 of a deployment named nginx + kubectl logs deployment/nginx -c nginx-1`) selectorTail int64 = 10 ) const ( - logsUsageStr = "expected 'logs POD_NAME [CONTAINER_NAME]'.\nPOD_NAME is a required argument for the logs command" + logsUsageStr = "expected 'logs (POD | TYPE/NAME) [CONTAINER_NAME]'.\nPOD or TYPE/NAME is a required argument for the logs command" ) type LogsOptions struct { @@ -83,9 +89,9 @@ type LogsOptions struct { func NewCmdLogs(f cmdutil.Factory, out io.Writer) *cobra.Command { o := &LogsOptions{} cmd := &cobra.Command{ - Use: "logs [-f] [-p] POD [-c CONTAINER]", + Use: "logs [-f] [-p] (POD | TYPE/NAME) [-c CONTAINER]", Short: i18n.T("Print the logs for a container in a pod"), - Long: "Print the logs for a container in a pod. If the pod has only one container, the container name is optional.", + Long: "Print the logs for a container in a pod or specified resource. If the pod has only one container, the container name is optional.", Example: logs_example, PreRun: func(cmd *cobra.Command, args []string) { if len(os.Args) > 1 && os.Args[1] == "log" { @@ -94,9 +100,7 @@ func NewCmdLogs(f cmdutil.Factory, out io.Writer) *cobra.Command { }, Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Complete(f, out, cmd, args)) - if err := o.Validate(); err != nil { - cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error())) - } + cmdutil.CheckErr(o.Validate()) cmdutil.CheckErr(o.RunLogs()) }, Aliases: []string{"log"}, diff --git a/pkg/kubectl/cmd/util/BUILD b/pkg/kubectl/cmd/util/BUILD index 884cd375f5c..cdf859d900f 100644 --- a/pkg/kubectl/cmd/util/BUILD +++ b/pkg/kubectl/cmd/util/BUILD @@ -75,6 +75,7 @@ go_test( name = "go_default_test", srcs = [ "cached_discovery_test.go", + "factory_object_mapping_test.go", "factory_test.go", "helpers_test.go", "shortcut_restmapper_test.go", @@ -90,7 +91,10 @@ go_test( "//pkg/api/testing:go_default_library", "//pkg/api/v1:go_default_library", "//pkg/api/validation:go_default_library", + "//pkg/apis/apps:go_default_library", + "//pkg/apis/batch:go_default_library", "//pkg/apis/extensions:go_default_library", + "//pkg/client/clientset_generated/internalclientset:go_default_library", "//pkg/client/clientset_generated/internalclientset/fake:go_default_library", "//pkg/controller:go_default_library", "//pkg/kubectl:go_default_library", @@ -104,6 +108,7 @@ go_test( "//vendor:k8s.io/apimachinery/pkg/labels", "//vendor:k8s.io/apimachinery/pkg/runtime", "//vendor:k8s.io/apimachinery/pkg/runtime/schema", + "//vendor:k8s.io/apimachinery/pkg/util/diff", "//vendor:k8s.io/apimachinery/pkg/util/validation/field", "//vendor:k8s.io/apimachinery/pkg/version", "//vendor:k8s.io/apimachinery/pkg/watch", diff --git a/pkg/kubectl/cmd/util/factory_object_mapping.go b/pkg/kubectl/cmd/util/factory_object_mapping.go index 9e2d43639ab..57d398ea717 100644 --- a/pkg/kubectl/cmd/util/factory_object_mapping.go +++ b/pkg/kubectl/cmd/util/factory_object_mapping.go @@ -213,51 +213,48 @@ func (f *ring1Factory) LogsForObject(object, options runtime.Object) (*restclien if err != nil { return nil, err } + opts, ok := options.(*api.PodLogOptions) + if !ok { + return nil, errors.New("provided options object is not a PodLogOptions") + } + var selector labels.Selector + var namespace string switch t := object.(type) { case *api.Pod: - opts, ok := options.(*api.PodLogOptions) - if !ok { - return nil, errors.New("provided options object is not a PodLogOptions") - } return clientset.Core().Pods(t.Namespace).GetLogs(t.Name, opts), nil case *api.ReplicationController: - opts, ok := options.(*api.PodLogOptions) - if !ok { - return nil, errors.New("provided options object is not a PodLogOptions") - } - selector := labels.SelectorFromSet(t.Spec.Selector) - sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) } - pod, numPods, err := GetFirstPod(clientset.Core(), t.Namespace, selector, 20*time.Second, sortBy) - if err != nil { - return nil, err - } - if numPods > 1 { - fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name) - } - - return clientset.Core().Pods(pod.Namespace).GetLogs(pod.Name, opts), nil + namespace = t.Namespace + selector = labels.SelectorFromSet(t.Spec.Selector) case *extensions.ReplicaSet: - opts, ok := options.(*api.PodLogOptions) - if !ok { - return nil, errors.New("provided options object is not a PodLogOptions") - } - selector, err := metav1.LabelSelectorAsSelector(t.Spec.Selector) + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) if err != nil { return nil, fmt.Errorf("invalid label selector: %v", err) } - sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) } - pod, numPods, err := GetFirstPod(clientset.Core(), t.Namespace, selector, 20*time.Second, sortBy) + + case *extensions.Deployment: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) if err != nil { - return nil, err - } - if numPods > 1 { - fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name) + return nil, fmt.Errorf("invalid label selector: %v", err) } - return clientset.Core().Pods(pod.Namespace).GetLogs(pod.Name, opts), nil + case *batch.Job: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return nil, fmt.Errorf("invalid label selector: %v", err) + } + + case *apps.StatefulSet: + namespace = t.Namespace + selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) + if err != nil { + return nil, fmt.Errorf("invalid label selector: %v", err) + } default: gvks, _, err := api.Scheme.ObjectKinds(object) @@ -266,6 +263,16 @@ func (f *ring1Factory) LogsForObject(object, options runtime.Object) (*restclien } return nil, fmt.Errorf("cannot get the logs from %v", gvks[0]) } + + sortBy := func(pods []*v1.Pod) sort.Interface { return controller.ByLogging(pods) } + pod, numPods, err := GetFirstPod(clientset.Core(), namespace, selector, 20*time.Second, sortBy) + if err != nil { + return nil, err + } + if numPods > 1 { + fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name) + } + return clientset.Core().Pods(pod.Namespace).GetLogs(pod.Name, opts), nil } func (f *ring1Factory) Scaler(mapping *meta.RESTMapping) (kubectl.Scaler, error) { @@ -329,29 +336,35 @@ func (f *ring1Factory) AttachablePodForObject(object runtime.Object) (*api.Pod, case *extensions.ReplicaSet: namespace = t.Namespace selector = labels.SelectorFromSet(t.Spec.Selector.MatchLabels) + case *api.ReplicationController: namespace = t.Namespace selector = labels.SelectorFromSet(t.Spec.Selector) + case *apps.StatefulSet: namespace = t.Namespace selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) if err != nil { return nil, fmt.Errorf("invalid label selector: %v", err) } + case *extensions.Deployment: namespace = t.Namespace selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) if err != nil { return nil, fmt.Errorf("invalid label selector: %v", err) } + case *batch.Job: namespace = t.Namespace selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) if err != nil { return nil, fmt.Errorf("invalid label selector: %v", err) } + case *api.Pod: return t, nil + default: gvks, _, err := api.Scheme.ObjectKinds(object) if err != nil { @@ -359,6 +372,7 @@ func (f *ring1Factory) AttachablePodForObject(object runtime.Object) (*api.Pod, } return nil, fmt.Errorf("cannot attach to %v: not implemented", gvks[0]) } + sortBy := func(pods []*v1.Pod) sort.Interface { return sort.Reverse(controller.ActivePods(pods)) } pod, _, err := GetFirstPod(clientset.Core(), namespace, selector, 1*time.Minute, sortBy) return pod, err diff --git a/pkg/kubectl/cmd/util/factory_object_mapping_test.go b/pkg/kubectl/cmd/util/factory_object_mapping_test.go new file mode 100644 index 00000000000..290b7004b7c --- /dev/null +++ b/pkg/kubectl/cmd/util/factory_object_mapping_test.go @@ -0,0 +1,192 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/diff" + testclient "k8s.io/client-go/testing" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/apps" + "k8s.io/kubernetes/pkg/apis/batch" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake" +) + +type fakeClientAccessFactory struct { + ClientAccessFactory + + fakeClientset *fake.Clientset +} + +func (f *fakeClientAccessFactory) ClientSetForVersion(requiredVersion *schema.GroupVersion) (internalclientset.Interface, error) { + return f.fakeClientset, nil +} + +func newFakeClientAccessFactory(objs []runtime.Object) *fakeClientAccessFactory { + return &fakeClientAccessFactory{ + fakeClientset: fake.NewSimpleClientset(objs...), + } +} + +var ( + podsResource = schema.GroupVersionResource{Resource: "pods"} +) + +func TestLogsForObject(t *testing.T) { + tests := []struct { + name string + obj runtime.Object + opts *api.PodLogOptions + pods []runtime.Object + actions []testclient.Action + }{ + { + name: "pod logs", + obj: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + getLogsAction("test", nil), + }, + }, + { + name: "replication controller logs", + obj: &api.ReplicationController{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: api.ReplicationControllerSpec{ + Selector: map[string]string{"foo": "bar"}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "replica set logs", + obj: &extensions.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: extensions.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "deployment logs", + obj: &extensions.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: extensions.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "job logs", + obj: &batch.Job{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: batch.JobSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + { + name: "stateful set logs", + obj: &apps.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{Name: "hello", Namespace: "test"}, + Spec: apps.StatefulSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + }, + }, + pods: []runtime.Object{testPod()}, + actions: []testclient.Action{ + testclient.NewListAction(podsResource, "test", metav1.ListOptions{LabelSelector: "foo=bar"}), + getLogsAction("test", nil), + }, + }, + } + + for _, test := range tests { + caf := newFakeClientAccessFactory(test.pods) + omf := NewObjectMappingFactory(caf) + _, err := omf.LogsForObject(test.obj, test.opts) + if err != nil { + t.Errorf("%s: unexpected error: %v", test.name, err) + continue + } + for i := range test.actions { + if len(caf.fakeClientset.Actions()) < i { + t.Errorf("%s: action %d does not exists in actual actions: %#v", + test.name, i, caf.fakeClientset.Actions()) + continue + } + got := caf.fakeClientset.Actions()[i] + want := test.actions[i] + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: unexpected action: %s", test.name, diff.ObjectDiff(got, want)) + } + } + } +} + +func testPod() runtime.Object { + return &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "test", + Labels: map[string]string{"foo": "bar"}, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "c1"}}, + }, + } +} + +func getLogsAction(namespace string, opts *api.PodLogOptions) testclient.Action { + action := testclient.GenericActionImpl{} + action.Verb = "get" + action.Namespace = namespace + action.Resource = podsResource + action.Subresource = "logs" + action.Value = opts + return action +}