Merge pull request #10707 from kargakis/logs-with-resource-builder

logs: Use resource builder
This commit is contained in:
Wojciech Tyczynski 2015-11-09 16:40:10 +01:00
commit d80e0e837c
10 changed files with 257 additions and 137 deletions

View File

@ -19,15 +19,15 @@ Print the logs for a container in a pod. If the pod has only one container, the
.SH OPTIONS
.PP
\fB\-c\fP, \fB\-\-container\fP=""
Container name
Print the logs of this container
.PP
\fB\-f\fP, \fB\-\-follow\fP=false
Specify if the logs should be streamed.
.PP
\fB\-\-interactive\fP=true
If true, prompt the user for input when required. Default true.
\fB\-\-interactive\fP=false
If true, prompt the user for input when required.
.PP
\fB\-\-limit\-bytes\fP=0

View File

@ -66,7 +66,7 @@ $ kubectl logs --since=1h nginx
### Options
```
-c, --container="": Container name
-c, --container="": Print the logs of this container
-f, --follow[=false]: Specify if the logs should be streamed.
--limit-bytes=0: Maximum bytes of logs to return. Defaults to no limit.
-p, --previous[=false]: If true, print the logs for the previous instance of the container in a pod if it exists.
@ -108,7 +108,7 @@ $ kubectl logs --since=1h nginx
* [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager
###### Auto generated by spf13/cobra on 14-Oct-2015
###### Auto generated by spf13/cobra on 28-Oct-2015
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/user-guide/kubectl/kubectl_logs.md?pixel)]()

View File

@ -38,6 +38,7 @@ type PodInterface interface {
Watch(label labels.Selector, field fields.Selector, opts api.ListOptions) (watch.Interface, error)
Bind(binding *api.Binding) error
UpdateStatus(pod *api.Pod) (*api.Pod, error)
GetLogs(name string, opts *api.PodLogOptions) *Request
}
// pods implements PodsNamespacer interface
@ -118,3 +119,8 @@ func (c *pods) UpdateStatus(pod *api.Pod) (result *api.Pod, err error) {
err = c.r.Put().Namespace(c.ns).Resource("pods").Name(pod.Name).SubResource("status").Body(pod).Do().Into(result)
return
}
// Get constructs a request for getting the logs for a pod
func (c *pods) GetLogs(name string, opts *api.PodLogOptions) *Request {
return c.r.Get().Namespace(c.ns).Name(name).Resource("pods").SubResource("log").VersionedParams(opts, api.Scheme)
}

View File

@ -187,3 +187,29 @@ func TestUpdatePod(t *testing.T) {
receivedPod, err := c.Setup(t).Pods(ns).Update(requestPod)
c.Validate(t, receivedPod, err)
}
func TestPodGetLogs(t *testing.T) {
ns := api.NamespaceDefault
opts := &api.PodLogOptions{
Follow: true,
Timestamps: true,
}
c := &testClient{}
request := c.Setup(t).Pods(ns).GetLogs("podName", opts)
if request.verb != "GET" {
t.Fatalf("unexpected verb %q, expected %q", request.verb, "GET")
}
if request.resource != "pods" {
t.Fatalf("unexpected resource %q, expected %q", request.subresource, "pods")
}
if request.subresource != "log" {
t.Fatalf("unexpected subresource %q, expected %q", request.subresource, "log")
}
expected := map[string]string{"container": "", "follow": "true", "previous": "false", "timestamps": "true"}
for gotKey, gotValue := range request.params {
if gotValue[0] != expected[gotKey] {
t.Fatalf("unexpected key-value %s=%s, expected %s=%s", gotKey, gotValue[0], gotKey, expected[gotKey])
}
}
}

View File

@ -18,6 +18,7 @@ package testclient
import (
"k8s.io/kubernetes/pkg/api"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/fields"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/watch"
@ -99,3 +100,15 @@ func (c *FakePods) UpdateStatus(pod *api.Pod) (*api.Pod, error) {
return obj.(*api.Pod), err
}
func (c *FakePods) GetLogs(name string, opts *api.PodLogOptions) *client.Request {
action := GenericActionImpl{}
action.Verb = "get"
action.Namespace = c.Namespace
action.Resource = "pod"
action.Subresource = "logs"
action.Value = opts
_, _ = c.Fake.Invokes(action, &api.Pod{})
return &client.Request{}
}

View File

@ -232,6 +232,26 @@ func NewAPIFactory() (*cmdutil.Factory, *testFactory, runtime.Codec) {
generator, ok := generators[name]
return generator, ok
},
LogsForObject: func(object, options runtime.Object) (*client.Request, error) {
fakeClient := t.Client.(*fake.RESTClient)
c := client.NewOrDie(t.ClientConfig)
c.Client = fakeClient.Client
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 c.Pods(t.Namespace).GetLogs(t.Name, opts), nil
default:
_, kind, err := api.Scheme.ObjectVersionAndKind(object)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("cannot get the logs from %s", kind)
}
},
}
rf := cmdutil.NewFactory(nil)
f.PodSelectorForObject = rf.PodSelectorForObject

View File

@ -18,19 +18,21 @@ package cmd
import (
"errors"
"fmt"
"io"
"math"
"os"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/meta"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/api/validation"
client "k8s.io/kubernetes/pkg/client/unversioned"
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
"k8s.io/kubernetes/pkg/kubectl/resource"
"k8s.io/kubernetes/pkg/runtime"
kerrors "k8s.io/kubernetes/pkg/util/errors"
)
const (
@ -51,193 +53,153 @@ $ kubectl logs --since=1h nginx`
)
type LogsOptions struct {
Client *client.Client
Namespace string
ResourceArg string
Options runtime.Object
PodNamespace string
PodName string
ContainerName string
Follow bool
Timestamps bool
Previous bool
LimitBytes int
Tail int
SinceTime *unversioned.Time
SinceSeconds time.Duration
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
ClientMapper resource.ClientMapper
LogsForObject func(object, options runtime.Object) (*client.Request, error)
Out io.Writer
}
// NewCmdLog creates a new pod log command
func NewCmdLog(f *cmdutil.Factory, out io.Writer) *cobra.Command {
o := &LogsOptions{
Out: out,
Tail: -1,
}
o := &LogsOptions{}
cmd := &cobra.Command{
Use: "logs [-f] [-p] POD [-c CONTAINER]",
Short: "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.",
Example: log_example,
Run: func(cmd *cobra.Command, args []string) {
PreRun: func(cmd *cobra.Command, args []string) {
if len(os.Args) > 1 && os.Args[1] == "log" {
printDeprecationWarning("logs", "log")
}
},
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.RunLog())
_, err := o.RunLog()
cmdutil.CheckErr(err)
},
Aliases: []string{"log"},
}
cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.")
cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output")
cmd.Flags().Bool("interactive", true, "If true, prompt the user for input when required. Default true.")
cmd.Flags().MarkDeprecated("interactive", "This flag is no longer respected and there is no replacement.")
cmd.Flags().IntVar(&o.LimitBytes, "limit-bytes", o.LimitBytes, "Maximum bytes of logs to return. Defaults to no limit.")
cmd.Flags().BoolVarP(&o.Previous, "previous", "p", o.Previous, "If true, print the logs for the previous instance of the container in a pod if it exists.")
cmd.Flags().IntVar(&o.Tail, "tail", o.Tail, "Lines of recent log file to display. Defaults to -1, showing all log lines.")
cmd.Flags().BoolP("follow", "f", false, "Specify if the logs should be streamed.")
cmd.Flags().Bool("timestamps", false, "Include timestamps on each line in the log output")
cmd.Flags().Int64("limit-bytes", 0, "Maximum bytes of logs to return. Defaults to no limit.")
cmd.Flags().BoolP("previous", "p", false, "If true, print the logs for the previous instance of the container in a pod if it exists.")
cmd.Flags().Int64("tail", -1, "Lines of recent log file to display. Defaults to -1, showing all log lines.")
cmd.Flags().String("since-time", "", "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.")
cmd.Flags().DurationVar(&o.SinceSeconds, "since", o.SinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.")
cmd.Flags().StringVarP(&o.ContainerName, "container", "c", o.ContainerName, "Container name")
cmd.Flags().Duration("since", 0, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.")
cmd.Flags().StringP("container", "c", "", "Print the logs of this container")
cmd.Flags().Bool("interactive", false, "If true, prompt the user for input when required.")
cmd.Flags().MarkDeprecated("interactive", "This flag is no longer respected and there is no replacement.")
return cmd
}
func (o *LogsOptions) Complete(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error {
containerName := cmdutil.GetFlagString(cmd, "container")
switch len(args) {
case 0:
return cmdutil.UsageError(cmd, "POD is required for log")
return cmdutil.UsageError(cmd, "POD is required for logs")
case 1:
o.PodName = args[0]
o.ResourceArg = args[0]
case 2:
if cmd.Flag("container").Changed {
return cmdutil.UsageError(cmd, "only one of -c, [CONTAINER] arg is allowed")
}
o.PodName = args[0]
o.ContainerName = args[1]
o.ResourceArg = args[0]
containerName = args[1]
default:
return cmdutil.UsageError(cmd, "log POD [-c CONTAINER]")
return cmdutil.UsageError(cmd, "logs POD [-c CONTAINER]")
}
var err error
o.PodNamespace, _, err = f.DefaultNamespace()
if err != nil {
return err
}
o.Client, err = f.Client()
o.Namespace, _, err = f.DefaultNamespace()
if err != nil {
return err
}
sinceTime := cmdutil.GetFlagString(cmd, "since-time")
if len(sinceTime) > 0 {
logOptions := &api.PodLogOptions{
Container: containerName,
Follow: cmdutil.GetFlagBool(cmd, "follow"),
Previous: cmdutil.GetFlagBool(cmd, "previous"),
Timestamps: cmdutil.GetFlagBool(cmd, "timestamps"),
}
if sinceTime := cmdutil.GetFlagString(cmd, "since-time"); len(sinceTime) > 0 {
t, err := api.ParseRFC3339(sinceTime, unversioned.Now)
if err != nil {
return err
}
o.SinceTime = &t
logOptions.SinceTime = &t
}
if limit := cmdutil.GetFlagInt64(cmd, "limit-bytes"); limit != 0 {
logOptions.LimitBytes = &limit
}
if tail := cmdutil.GetFlagInt64(cmd, "tail"); tail != -1 {
logOptions.TailLines = &tail
}
if sinceSeconds := cmdutil.GetFlagDuration(cmd, "since"); sinceSeconds != 0 {
// round up to the nearest second
sec := int64(math.Ceil(float64(sinceSeconds) / float64(time.Second)))
logOptions.SinceSeconds = &sec
}
o.Options = logOptions
o.Mapper, o.Typer = f.Object()
o.ClientMapper = f.ClientMapperForCommand()
o.LogsForObject = f.LogsForObject
o.Out = out
return nil
}
func (o *LogsOptions) Validate() error {
if len(o.PodName) == 0 {
return errors.New("POD must be specified")
func (o LogsOptions) Validate() error {
if len(o.ResourceArg) == 0 {
return errors.New("a pod must be specified")
}
if o.LimitBytes < 0 {
return errors.New("--limit-bytes must be greater than or equal to zero")
logOptions, ok := o.Options.(*api.PodLogOptions)
if !ok {
return errors.New("unexpected log options object")
}
if o.Tail < -1 {
return errors.New("--tail must be greater than or equal to -1")
}
if o.SinceTime != nil && o.SinceSeconds > 0 {
return errors.New("only one of --since, --since-time may be specified")
if errs := validation.ValidatePodLogOptions(logOptions); len(errs) > 0 {
return kerrors.NewAggregate(errs)
}
return nil
}
// RunLog retrieves a pod log
func (o *LogsOptions) RunLog() error {
pod, err := o.Client.Pods(o.PodNamespace).Get(o.PodName)
func (o LogsOptions) RunLog() (int64, error) {
infos, err := resource.NewBuilder(o.Mapper, o.Typer, o.ClientMapper).
NamespaceParam(o.Namespace).DefaultNamespace().
ResourceNames("pods", o.ResourceArg).
SingleResourceType().
Do().Infos()
if err != nil {
return err
return 0, err
}
if len(infos) != 1 {
return 0, errors.New("expected a resource")
}
info := infos[0]
req, err := o.LogsForObject(info.Object, o.Options)
if err != nil {
return 0, err
}
// [-c CONTAINER]
container := o.ContainerName
if len(container) == 0 {
// [CONTAINER] (container as arg not flag) is supported as legacy behavior. See PR #10519 for more details.
if len(pod.Spec.Containers) != 1 {
podContainersNames := []string{}
for _, container := range pod.Spec.Containers {
podContainersNames = append(podContainersNames, container.Name)
}
return fmt.Errorf("Pod %s has the following containers: %s; please specify the container to print logs for with -c", pod.ObjectMeta.Name, strings.Join(podContainersNames, ", "))
}
container = pod.Spec.Containers[0].Name
}
logOptions := &api.PodLogOptions{
Container: container,
Follow: o.Follow,
Previous: o.Previous,
Timestamps: o.Timestamps,
}
if o.SinceSeconds > 0 {
// round up to the nearest second
sec := int64(math.Ceil(float64(o.SinceSeconds) / float64(time.Second)))
logOptions.SinceSeconds = &sec
}
logOptions.SinceTime = o.SinceTime
if o.LimitBytes != 0 {
i := int64(o.LimitBytes)
logOptions.LimitBytes = &i
}
if o.Tail >= 0 {
i := int64(o.Tail)
logOptions.TailLines = &i
}
return handleLog(o.Client, o.PodNamespace, o.PodName, logOptions, o.Out)
}
func handleLog(client *client.Client, namespace, podID string, logOptions *api.PodLogOptions, out io.Writer) error {
// TODO: transform this into a PodLogOptions call
req := client.RESTClient.Get().
Namespace(namespace).
Name(podID).
Resource("pods").
SubResource("log").
Param("follow", strconv.FormatBool(logOptions.Follow)).
Param("container", logOptions.Container).
Param("previous", strconv.FormatBool(logOptions.Previous)).
Param("timestamps", strconv.FormatBool(logOptions.Timestamps))
if logOptions.SinceSeconds != nil {
req.Param("sinceSeconds", strconv.FormatInt(*logOptions.SinceSeconds, 10))
}
if logOptions.SinceTime != nil {
req.Param("sinceTime", logOptions.SinceTime.Format(time.RFC3339))
}
if logOptions.LimitBytes != nil {
req.Param("limitBytes", strconv.FormatInt(*logOptions.LimitBytes, 10))
}
if logOptions.TailLines != nil {
req.Param("tailLines", strconv.FormatInt(*logOptions.TailLines, 10))
}
readCloser, err := req.Stream()
if err != nil {
return err
return 0, err
}
defer readCloser.Close()
_, err = io.Copy(out, readCloser)
return err
return io.Copy(o.Out, readCloser)
}

View File

@ -20,8 +20,12 @@ import (
"bytes"
"io/ioutil"
"net/http"
"os"
"strings"
"testing"
"github.com/spf13/cobra"
"k8s.io/kubernetes/pkg/api"
client "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/fake"
@ -35,7 +39,7 @@ func TestLog(t *testing.T) {
{
name: "v1 - pod log",
version: "v1",
podPath: "/api/v1/namespaces/test/pods/foo",
podPath: "/namespaces/test/pods/foo",
logPath: "/api/v1/namespaces/test/pods/foo/log",
pod: testPod(),
},
@ -88,3 +92,49 @@ func testPod() *api.Pod {
},
}
}
func TestValidateLogFlags(t *testing.T) {
f, _, _ := NewAPIFactory()
tests := []struct {
name string
flags map[string]string
expected string
}{
{
name: "since & since-time",
flags: map[string]string{"since": "1h", "since-time": "2006-01-02T15:04:05Z"},
expected: "only one of sinceTime or sinceSeconds can be provided",
},
{
name: "negative limit-bytes",
flags: map[string]string{"limit-bytes": "-100"},
expected: "limitBytes must be a positive integer or nil",
},
{
name: "negative tail",
flags: map[string]string{"tail": "-100"},
expected: "tailLines must be a non-negative integer or nil",
},
}
for _, test := range tests {
cmd := NewCmdLog(f, bytes.NewBuffer([]byte{}))
out := ""
for flag, value := range test.flags {
cmd.Flags().Set(flag, value)
}
// checkErr breaks tests in case of errors, plus we just
// need to check errors returned by the command validation
o := &LogsOptions{}
cmd.Run = func(cmd *cobra.Command, args []string) {
o.Complete(f, os.Stdout, cmd, args)
out = o.Validate().Error()
o.RunLog()
}
cmd.Run(cmd, []string{"foo"})
if !strings.Contains(out, test.expected) {
t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, out)
}
}
}

View File

@ -206,7 +206,7 @@ func Run(f *cmdutil.Factory, cmdIn io.Reader, cmdOut, cmdErr io.Writer, cmd *cob
if err != nil {
return err
}
return handleAttachPod(client, attachablePod, opts)
return handleAttachPod(f, client, attachablePod, opts)
}
outputFormat := cmdutil.GetFlagString(cmd, "output")
@ -245,20 +245,40 @@ func waitForPodRunning(c *client.Client, pod *api.Pod, out io.Writer) (status ap
}
}
func handleAttachPod(c *client.Client, pod *api.Pod, opts *AttachOptions) error {
func handleAttachPod(f *cmdutil.Factory, c *client.Client, pod *api.Pod, opts *AttachOptions) error {
status, err := waitForPodRunning(c, pod, opts.Out)
if err != nil {
return err
}
if status == api.PodSucceeded || status == api.PodFailed {
return handleLog(c, pod.Namespace, pod.Name, &api.PodLogOptions{Container: opts.GetContainerName(pod)}, opts.Out)
req, err := f.LogsForObject(pod, &api.PodLogOptions{Container: opts.GetContainerName(pod)})
if err != nil {
return err
}
readCloser, err := req.Stream()
if err != nil {
return err
}
defer readCloser.Close()
_, err = io.Copy(opts.Out, readCloser)
return err
}
opts.Client = c
opts.PodName = pod.Name
opts.Namespace = pod.Namespace
if err := opts.Run(); err != nil {
fmt.Fprintf(opts.Out, "Error attaching, falling back to logs: %v\n", err)
return handleLog(c, pod.Namespace, pod.Name, &api.PodLogOptions{Container: opts.GetContainerName(pod)}, opts.Out)
req, err := f.LogsForObject(pod, &api.PodLogOptions{Container: opts.GetContainerName(pod)})
if err != nil {
return err
}
readCloser, err := req.Stream()
if err != nil {
return err
}
defer readCloser.Close()
_, err = io.Copy(opts.Out, readCloser)
return err
}
return nil
}

View File

@ -83,6 +83,8 @@ type Factory struct {
PortsForObject func(object runtime.Object) ([]string, error)
// LabelsForObject returns the labels associated with the provided object
LabelsForObject func(object runtime.Object) (map[string]string, error)
// LogsForObject returns a request for the logs associated with the provided object
LogsForObject func(object, options runtime.Object) (*client.Request, error)
// Returns a schema that can validate objects stored on disk.
Validator func(validate bool, cacheDir string) (validation.Schema, error)
// Returns the default namespace to use in cases where no
@ -218,6 +220,27 @@ func NewFactory(optionalClientConfig clientcmd.ClientConfig) *Factory {
LabelsForObject: func(object runtime.Object) (map[string]string, error) {
return meta.NewAccessor().Labels(object)
},
LogsForObject: func(object, options runtime.Object) (*client.Request, error) {
c, err := clients.ClientForVersion("")
if err != nil {
return nil, err
}
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 c.Pods(t.Namespace).GetLogs(t.Name, opts), nil
default:
_, kind, err := api.Scheme.ObjectVersionAndKind(object)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("cannot get the logs from %s", kind)
}
},
Scaler: func(mapping *meta.RESTMapping) (kubectl.Scaler, error) {
client, err := clients.ClientForVersion(mapping.APIVersion)
if err != nil {