mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-27 13:37:30 +00:00
Merge pull request #13780 from smarterclayton/pod_logs
Auto commit by PR queue bot
This commit is contained in:
commit
c96c76b729
@ -6062,6 +6062,46 @@
|
|||||||
"required": false,
|
"required": false,
|
||||||
"allowMultiple": false
|
"allowMultiple": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "*int64",
|
||||||
|
"paramType": "query",
|
||||||
|
"name": "sinceSeconds",
|
||||||
|
"description": "A relative time in seconds before the current time from which to show logs. If this value precedes the time a pod was started, only logs since the pod start will be returned. If this value is in the future, no logs will be returned. Only one of sinceSeconds or sinceTime may be specified.",
|
||||||
|
"required": false,
|
||||||
|
"allowMultiple": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "*unversioned.Time",
|
||||||
|
"paramType": "query",
|
||||||
|
"name": "sinceTime",
|
||||||
|
"description": "An RFC3339 timestamp from which to show logs. If this value preceeds the time a pod was started, only logs since the pod start will be returned. If this value is in the future, no logs will be returned. Only one of sinceSeconds or sinceTime may be specified.",
|
||||||
|
"required": false,
|
||||||
|
"allowMultiple": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"paramType": "query",
|
||||||
|
"name": "timestamps",
|
||||||
|
"description": "If true, add an RFC3339 or RFC3339Nano timestamp at the beginning of every line of log output. Defaults to false.",
|
||||||
|
"required": false,
|
||||||
|
"allowMultiple": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "*int64",
|
||||||
|
"paramType": "query",
|
||||||
|
"name": "tailLines",
|
||||||
|
"description": "If set, the number of lines from the end of the logs to show. If not specified, logs are shown from the creation of the container or sinceSeconds or sinceTime",
|
||||||
|
"required": false,
|
||||||
|
"allowMultiple": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "*int64",
|
||||||
|
"paramType": "query",
|
||||||
|
"name": "limitBytes",
|
||||||
|
"description": "If set, the number of bytes to read from the server before terminating the log output. This may not display a complete final line of logging, and may return slightly more or slightly less than the specified limit.",
|
||||||
|
"required": false,
|
||||||
|
"allowMultiple": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path",
|
"paramType": "path",
|
||||||
|
@ -533,8 +533,13 @@ _kubectl_logs()
|
|||||||
flags+=("--follow")
|
flags+=("--follow")
|
||||||
flags+=("-f")
|
flags+=("-f")
|
||||||
flags+=("--interactive")
|
flags+=("--interactive")
|
||||||
|
flags+=("--limit-bytes=")
|
||||||
flags+=("--previous")
|
flags+=("--previous")
|
||||||
flags+=("-p")
|
flags+=("-p")
|
||||||
|
flags+=("--since=")
|
||||||
|
flags+=("--since-time=")
|
||||||
|
flags+=("--tail=")
|
||||||
|
flags+=("--timestamps")
|
||||||
|
|
||||||
must_have_one_flag=()
|
must_have_one_flag=()
|
||||||
must_have_one_noun=()
|
must_have_one_noun=()
|
||||||
|
@ -29,10 +29,30 @@ Print the logs for a container in a pod. If the pod has only one container, the
|
|||||||
\fB\-\-interactive\fP=true
|
\fB\-\-interactive\fP=true
|
||||||
If true, prompt the user for input when required. Default true.
|
If true, prompt the user for input when required. Default true.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-limit\-bytes\fP=0
|
||||||
|
Maximum bytes of logs to return. Defaults to no limit.
|
||||||
|
|
||||||
.PP
|
.PP
|
||||||
\fB\-p\fP, \fB\-\-previous\fP=false
|
\fB\-p\fP, \fB\-\-previous\fP=false
|
||||||
If true, print the logs for the previous instance of the container in a pod if it exists.
|
If true, print the logs for the previous instance of the container in a pod if it exists.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-since\fP=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.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-since\-time\fP=""
|
||||||
|
Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since\-time / since may be used.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-tail\fP=\-1
|
||||||
|
Lines of recent log file to display. Defaults to \-1, showing all log lines.
|
||||||
|
|
||||||
|
.PP
|
||||||
|
\fB\-\-timestamps\fP=false
|
||||||
|
Include timestamps on each line in the log output
|
||||||
|
|
||||||
|
|
||||||
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
.SH OPTIONS INHERITED FROM PARENT COMMANDS
|
||||||
.PP
|
.PP
|
||||||
@ -133,14 +153,20 @@ Print the logs for a container in a pod. If the pod has only one container, the
|
|||||||
.RS
|
.RS
|
||||||
|
|
||||||
.nf
|
.nf
|
||||||
# Return snapshot of ruby\-container logs from pod 123456\-7890.
|
# Return snapshot logs from pod nginx with only one container
|
||||||
$ kubectl logs 123456\-7890 ruby\-container
|
$ kubectl logs nginx
|
||||||
|
|
||||||
# Return snapshot of previous terminated ruby\-container logs from pod 123456\-7890.
|
# Return snapshot of previous terminated ruby container logs from pod web\-1
|
||||||
$ kubectl logs \-p 123456\-7890 ruby\-container
|
$ kubectl logs \-p \-c ruby web\-1
|
||||||
|
|
||||||
# Start streaming of ruby\-container logs from pod 123456\-7890.
|
# Begin streaming the logs of the ruby container in pod web\-1
|
||||||
$ kubectl logs \-f 123456\-7890 ruby\-container
|
$ kubectl logs \-f \-c ruby web\-1
|
||||||
|
|
||||||
|
# Display only the most recent 20 lines of output in pod nginx
|
||||||
|
$ kubectl logs \-\-tail=20 nginx
|
||||||
|
|
||||||
|
# Show all logs from pod nginx written in the last hour
|
||||||
|
$ kubectl logs \-\-since=1h nginx
|
||||||
|
|
||||||
.fi
|
.fi
|
||||||
.RE
|
.RE
|
||||||
|
@ -47,14 +47,20 @@ kubectl logs [-f] [-p] POD [-c CONTAINER]
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```
|
```
|
||||||
# Return snapshot of ruby-container logs from pod 123456-7890.
|
# Return snapshot logs from pod nginx with only one container
|
||||||
$ kubectl logs 123456-7890 ruby-container
|
$ kubectl logs nginx
|
||||||
|
|
||||||
# Return snapshot of previous terminated ruby-container logs from pod 123456-7890.
|
# Return snapshot of previous terminated ruby container logs from pod web-1
|
||||||
$ kubectl logs -p 123456-7890 ruby-container
|
$ kubectl logs -p -c ruby web-1
|
||||||
|
|
||||||
# Start streaming of ruby-container logs from pod 123456-7890.
|
# Begin streaming the logs of the ruby container in pod web-1
|
||||||
$ kubectl logs -f 123456-7890 ruby-container
|
$ kubectl logs -f -c ruby web-1
|
||||||
|
|
||||||
|
# Display only the most recent 20 lines of output in pod nginx
|
||||||
|
$ kubectl logs --tail=20 nginx
|
||||||
|
|
||||||
|
# Show all logs from pod nginx written in the last hour
|
||||||
|
$ kubectl logs --since=1h nginx
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
@ -63,7 +69,12 @@ $ kubectl logs -f 123456-7890 ruby-container
|
|||||||
-c, --container="": Container name
|
-c, --container="": Container name
|
||||||
-f, --follow[=false]: Specify if the logs should be streamed.
|
-f, --follow[=false]: Specify if the logs should be streamed.
|
||||||
--interactive[=true]: If true, prompt the user for input when required. Default true.
|
--interactive[=true]: If true, prompt the user for input when required. Default true.
|
||||||
|
--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.
|
-p, --previous[=false]: If true, print the logs for the previous instance of the container in a pod if it exists.
|
||||||
|
--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.
|
||||||
|
--since-time="": Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.
|
||||||
|
--tail=-1: Lines of recent log file to display. Defaults to -1, showing all log lines.
|
||||||
|
--timestamps[=false]: Include timestamps on each line in the log output
|
||||||
```
|
```
|
||||||
|
|
||||||
### Options inherited from parent commands
|
### Options inherited from parent commands
|
||||||
@ -98,7 +109,7 @@ $ kubectl logs -f 123456-7890 ruby-container
|
|||||||
|
|
||||||
* [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-10 18:53:03.154570214 +0000 UTC
|
###### Auto generated by spf13/cobra at 2015-09-16 18:54:52.319210951 +0000 UTC
|
||||||
|
|
||||||
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
|
<!-- BEGIN MUNGE: GENERATED_ANALYTICS -->
|
||||||
[]()
|
[]()
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
accept-hosts
|
accept-hosts
|
||||||
accept-paths
|
accept-paths
|
||||||
account-for-pod-resources
|
account-for-pod-resources
|
||||||
@ -143,6 +144,7 @@ kube-master
|
|||||||
label-columns
|
label-columns
|
||||||
last-release-pr
|
last-release-pr
|
||||||
legacy-userspace-proxy
|
legacy-userspace-proxy
|
||||||
|
limit-bytes
|
||||||
load-balancer-ip
|
load-balancer-ip
|
||||||
log-flush-frequency
|
log-flush-frequency
|
||||||
long-running-request-regexp
|
long-running-request-regexp
|
||||||
@ -252,6 +254,8 @@ service-node-port-range
|
|||||||
service-node-ports
|
service-node-ports
|
||||||
service-sync-period
|
service-sync-period
|
||||||
session-affinity
|
session-affinity
|
||||||
|
since-seconds
|
||||||
|
since-time
|
||||||
show-all
|
show-all
|
||||||
shutdown-fd
|
shutdown-fd
|
||||||
shutdown-fifo
|
shutdown-fifo
|
||||||
@ -287,3 +291,4 @@ www-prefix
|
|||||||
retry_time
|
retry_time
|
||||||
file_content_in_loop
|
file_content_in_loop
|
||||||
cpu-cfs-quota
|
cpu-cfs-quota
|
||||||
|
|
||||||
|
@ -1430,6 +1430,33 @@ func deepCopy_api_PodLogOptions(in PodLogOptions, out *PodLogOptions, c *convers
|
|||||||
out.Container = in.Container
|
out.Container = in.Container
|
||||||
out.Follow = in.Follow
|
out.Follow = in.Follow
|
||||||
out.Previous = in.Previous
|
out.Previous = in.Previous
|
||||||
|
if in.SinceSeconds != nil {
|
||||||
|
out.SinceSeconds = new(int64)
|
||||||
|
*out.SinceSeconds = *in.SinceSeconds
|
||||||
|
} else {
|
||||||
|
out.SinceSeconds = nil
|
||||||
|
}
|
||||||
|
if in.SinceTime != nil {
|
||||||
|
out.SinceTime = new(unversioned.Time)
|
||||||
|
if err := deepCopy_unversioned_Time(*in.SinceTime, out.SinceTime, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.SinceTime = nil
|
||||||
|
}
|
||||||
|
out.Timestamps = in.Timestamps
|
||||||
|
if in.TailLines != nil {
|
||||||
|
out.TailLines = new(int64)
|
||||||
|
*out.TailLines = *in.TailLines
|
||||||
|
} else {
|
||||||
|
out.TailLines = nil
|
||||||
|
}
|
||||||
|
if in.LimitBytes != nil {
|
||||||
|
out.LimitBytes = new(int64)
|
||||||
|
*out.LimitBytes = *in.LimitBytes
|
||||||
|
} else {
|
||||||
|
out.LimitBytes = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/api/resource"
|
"k8s.io/kubernetes/pkg/api/resource"
|
||||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
@ -235,3 +236,15 @@ func containsAccessMode(modes []PersistentVolumeAccessMode, mode PersistentVolum
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseRFC3339 parses an RFC3339 date in either RFC3339Nano or RFC3339 format.
|
||||||
|
func ParseRFC3339(s string, nowFn func() unversioned.Time) (unversioned.Time, error) {
|
||||||
|
if t, timeErr := time.Parse(time.RFC3339Nano, s); timeErr == nil {
|
||||||
|
return unversioned.Time{t}, nil
|
||||||
|
}
|
||||||
|
t, err := time.Parse(time.RFC3339, s)
|
||||||
|
if err != nil {
|
||||||
|
return unversioned.Time{}, err
|
||||||
|
}
|
||||||
|
return unversioned.Time{t}, nil
|
||||||
|
}
|
||||||
|
@ -1598,12 +1598,30 @@ type PodLogOptions struct {
|
|||||||
|
|
||||||
// Container for which to return logs
|
// Container for which to return logs
|
||||||
Container string
|
Container string
|
||||||
|
|
||||||
// If true, follow the logs for the pod
|
// If true, follow the logs for the pod
|
||||||
Follow bool
|
Follow bool
|
||||||
|
|
||||||
// If true, return previous terminated container logs
|
// If true, return previous terminated container logs
|
||||||
Previous bool
|
Previous bool
|
||||||
|
// A relative time in seconds before the current time from which to show logs. If this value
|
||||||
|
// precedes the time a pod was started, only logs since the pod start will be returned.
|
||||||
|
// If this value is in the future, no logs will be returned.
|
||||||
|
// Only one of sinceSeconds or sinceTime may be specified.
|
||||||
|
SinceSeconds *int64
|
||||||
|
// An RFC3339 timestamp from which to show logs. If this value
|
||||||
|
// preceeds the time a pod was started, only logs since the pod start will be returned.
|
||||||
|
// If this value is in the future, no logs will be returned.
|
||||||
|
// Only one of sinceSeconds or sinceTime may be specified.
|
||||||
|
SinceTime *unversioned.Time
|
||||||
|
// If true, add an RFC3339 or RFC3339Nano timestamp at the beginning of every line
|
||||||
|
// of log output.
|
||||||
|
Timestamps bool
|
||||||
|
// If set, the number of lines from the end of the logs to show. If not specified,
|
||||||
|
// logs are shown from the creation of the container or sinceSeconds or sinceTime
|
||||||
|
TailLines *int64
|
||||||
|
// If set, the number of bytes to read from the server before terminating the
|
||||||
|
// log output. This may not display a complete final line of logging, and may return
|
||||||
|
// slightly more or slightly less than the specified limit.
|
||||||
|
LimitBytes *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodAttachOptions is the query options to a Pod's remote attach call
|
// PodAttachOptions is the query options to a Pod's remote attach call
|
||||||
|
@ -1648,6 +1648,32 @@ func convert_api_PodLogOptions_To_v1_PodLogOptions(in *api.PodLogOptions, out *P
|
|||||||
out.Container = in.Container
|
out.Container = in.Container
|
||||||
out.Follow = in.Follow
|
out.Follow = in.Follow
|
||||||
out.Previous = in.Previous
|
out.Previous = in.Previous
|
||||||
|
if in.SinceSeconds != nil {
|
||||||
|
out.SinceSeconds = new(int64)
|
||||||
|
*out.SinceSeconds = *in.SinceSeconds
|
||||||
|
} else {
|
||||||
|
out.SinceSeconds = nil
|
||||||
|
}
|
||||||
|
if in.SinceTime != nil {
|
||||||
|
if err := s.Convert(&in.SinceTime, &out.SinceTime, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.SinceTime = nil
|
||||||
|
}
|
||||||
|
out.Timestamps = in.Timestamps
|
||||||
|
if in.TailLines != nil {
|
||||||
|
out.TailLines = new(int64)
|
||||||
|
*out.TailLines = *in.TailLines
|
||||||
|
} else {
|
||||||
|
out.TailLines = nil
|
||||||
|
}
|
||||||
|
if in.LimitBytes != nil {
|
||||||
|
out.LimitBytes = new(int64)
|
||||||
|
*out.LimitBytes = *in.LimitBytes
|
||||||
|
} else {
|
||||||
|
out.LimitBytes = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4057,6 +4083,32 @@ func convert_v1_PodLogOptions_To_api_PodLogOptions(in *PodLogOptions, out *api.P
|
|||||||
out.Container = in.Container
|
out.Container = in.Container
|
||||||
out.Follow = in.Follow
|
out.Follow = in.Follow
|
||||||
out.Previous = in.Previous
|
out.Previous = in.Previous
|
||||||
|
if in.SinceSeconds != nil {
|
||||||
|
out.SinceSeconds = new(int64)
|
||||||
|
*out.SinceSeconds = *in.SinceSeconds
|
||||||
|
} else {
|
||||||
|
out.SinceSeconds = nil
|
||||||
|
}
|
||||||
|
if in.SinceTime != nil {
|
||||||
|
if err := s.Convert(&in.SinceTime, &out.SinceTime, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.SinceTime = nil
|
||||||
|
}
|
||||||
|
out.Timestamps = in.Timestamps
|
||||||
|
if in.TailLines != nil {
|
||||||
|
out.TailLines = new(int64)
|
||||||
|
*out.TailLines = *in.TailLines
|
||||||
|
} else {
|
||||||
|
out.TailLines = nil
|
||||||
|
}
|
||||||
|
if in.LimitBytes != nil {
|
||||||
|
out.LimitBytes = new(int64)
|
||||||
|
*out.LimitBytes = *in.LimitBytes
|
||||||
|
} else {
|
||||||
|
out.LimitBytes = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1450,6 +1450,33 @@ func deepCopy_v1_PodLogOptions(in PodLogOptions, out *PodLogOptions, c *conversi
|
|||||||
out.Container = in.Container
|
out.Container = in.Container
|
||||||
out.Follow = in.Follow
|
out.Follow = in.Follow
|
||||||
out.Previous = in.Previous
|
out.Previous = in.Previous
|
||||||
|
if in.SinceSeconds != nil {
|
||||||
|
out.SinceSeconds = new(int64)
|
||||||
|
*out.SinceSeconds = *in.SinceSeconds
|
||||||
|
} else {
|
||||||
|
out.SinceSeconds = nil
|
||||||
|
}
|
||||||
|
if in.SinceTime != nil {
|
||||||
|
out.SinceTime = new(unversioned.Time)
|
||||||
|
if err := deepCopy_unversioned_Time(*in.SinceTime, out.SinceTime, c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.SinceTime = nil
|
||||||
|
}
|
||||||
|
out.Timestamps = in.Timestamps
|
||||||
|
if in.TailLines != nil {
|
||||||
|
out.TailLines = new(int64)
|
||||||
|
*out.TailLines = *in.TailLines
|
||||||
|
} else {
|
||||||
|
out.TailLines = nil
|
||||||
|
}
|
||||||
|
if in.LimitBytes != nil {
|
||||||
|
out.LimitBytes = new(int64)
|
||||||
|
*out.LimitBytes = *in.LimitBytes
|
||||||
|
} else {
|
||||||
|
out.LimitBytes = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1982,14 +1982,30 @@ type PodLogOptions struct {
|
|||||||
|
|
||||||
// The container for which to stream logs. Defaults to only container if there is one container in the pod.
|
// The container for which to stream logs. Defaults to only container if there is one container in the pod.
|
||||||
Container string `json:"container,omitempty"`
|
Container string `json:"container,omitempty"`
|
||||||
|
// Follow the log stream of the pod. Defaults to false.
|
||||||
// Follow the log stream of the pod.
|
|
||||||
// Defaults to false.
|
|
||||||
Follow bool `json:"follow,omitempty"`
|
Follow bool `json:"follow,omitempty"`
|
||||||
|
// Return previous terminated container logs. Defaults to false.
|
||||||
// Return previous terminated container logs.
|
|
||||||
// Defaults to false.
|
|
||||||
Previous bool `json:"previous,omitempty"`
|
Previous bool `json:"previous,omitempty"`
|
||||||
|
// A relative time in seconds before the current time from which to show logs. If this value
|
||||||
|
// precedes the time a pod was started, only logs since the pod start will be returned.
|
||||||
|
// If this value is in the future, no logs will be returned.
|
||||||
|
// Only one of sinceSeconds or sinceTime may be specified.
|
||||||
|
SinceSeconds *int64 `json:"sinceSeconds,omitempty"`
|
||||||
|
// An RFC3339 timestamp from which to show logs. If this value
|
||||||
|
// preceeds the time a pod was started, only logs since the pod start will be returned.
|
||||||
|
// If this value is in the future, no logs will be returned.
|
||||||
|
// Only one of sinceSeconds or sinceTime may be specified.
|
||||||
|
SinceTime *unversioned.Time `json:"sinceTime,omitempty"`
|
||||||
|
// If true, add an RFC3339 or RFC3339Nano timestamp at the beginning of every line
|
||||||
|
// of log output. Defaults to false.
|
||||||
|
Timestamps bool `json:"timestamps,omitempty"`
|
||||||
|
// If set, the number of lines from the end of the logs to show. If not specified,
|
||||||
|
// logs are shown from the creation of the container or sinceSeconds or sinceTime
|
||||||
|
TailLines *int64 `json:"tailLines,omitempty"`
|
||||||
|
// If set, the number of bytes to read from the server before terminating the
|
||||||
|
// log output. This may not display a complete final line of logging, and may return
|
||||||
|
// slightly more or slightly less than the specified limit.
|
||||||
|
LimitBytes *int64 `json:"limitBytes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PodAttachOptions is the query options to a Pod's remote attach call.
|
// PodAttachOptions is the query options to a Pod's remote attach call.
|
||||||
|
@ -953,6 +953,11 @@ var map_PodLogOptions = map[string]string{
|
|||||||
"container": "The container for which to stream logs. Defaults to only container if there is one container in the pod.",
|
"container": "The container for which to stream logs. Defaults to only container if there is one container in the pod.",
|
||||||
"follow": "Follow the log stream of the pod. Defaults to false.",
|
"follow": "Follow the log stream of the pod. Defaults to false.",
|
||||||
"previous": "Return previous terminated container logs. Defaults to false.",
|
"previous": "Return previous terminated container logs. Defaults to false.",
|
||||||
|
"sinceSeconds": "A relative time in seconds before the current time from which to show logs. If this value precedes the time a pod was started, only logs since the pod start will be returned. If this value is in the future, no logs will be returned. Only one of sinceSeconds or sinceTime may be specified.",
|
||||||
|
"sinceTime": "An RFC3339 timestamp from which to show logs. If this value preceeds the time a pod was started, only logs since the pod start will be returned. If this value is in the future, no logs will be returned. Only one of sinceSeconds or sinceTime may be specified.",
|
||||||
|
"timestamps": "If true, add an RFC3339 or RFC3339Nano timestamp at the beginning of every line of log output. Defaults to false.",
|
||||||
|
"tailLines": "If set, the number of lines from the end of the logs to show. If not specified, logs are shown from the creation of the container or sinceSeconds or sinceTime",
|
||||||
|
"limitBytes": "If set, the number of bytes to read from the server before terminating the log output. This may not display a complete final line of logging, and may return slightly more or slightly less than the specified limit.",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (PodLogOptions) SwaggerDoc() map[string]string {
|
func (PodLogOptions) SwaggerDoc() map[string]string {
|
||||||
|
@ -1950,3 +1950,23 @@ func ValidateThirdPartyResource(obj *api.ThirdPartyResource) errs.ValidationErro
|
|||||||
func ValidateSchemaUpdate(oldResource, newResource *api.ThirdPartyResource) errs.ValidationErrorList {
|
func ValidateSchemaUpdate(oldResource, newResource *api.ThirdPartyResource) errs.ValidationErrorList {
|
||||||
return errs.ValidationErrorList{fmt.Errorf("Schema update is not supported.")}
|
return errs.ValidationErrorList{fmt.Errorf("Schema update is not supported.")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidatePodLogOptions(opts *api.PodLogOptions) errs.ValidationErrorList {
|
||||||
|
allErrs := errs.ValidationErrorList{}
|
||||||
|
if opts.TailLines != nil && *opts.TailLines < 0 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("tailLines", *opts.TailLines, "tailLines must be a non-negative integer or nil"))
|
||||||
|
}
|
||||||
|
if opts.LimitBytes != nil && *opts.LimitBytes < 1 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("limitBytes", *opts.LimitBytes, "limitBytes must be a positive integer or nil"))
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case opts.SinceSeconds != nil && opts.SinceTime != nil:
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("sinceSeconds", *opts.SinceSeconds, "only one of sinceTime or sinceSeconds can be provided"))
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("sinceTime", *opts.SinceTime, "only one of sinceTime or sinceSeconds can be provided"))
|
||||||
|
case opts.SinceSeconds != nil:
|
||||||
|
if *opts.SinceSeconds < 1 {
|
||||||
|
allErrs = append(allErrs, errs.NewFieldInvalid("sinceSeconds", *opts.SinceSeconds, "sinceSeconds must be a positive integer"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
@ -3866,3 +3866,34 @@ func fakeValidSecurityContext(priv bool) *api.SecurityContext {
|
|||||||
Privileged: &priv,
|
Privileged: &priv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidPodLogOptions(t *testing.T) {
|
||||||
|
now := unversioned.Now()
|
||||||
|
negative := int64(-1)
|
||||||
|
zero := int64(0)
|
||||||
|
positive := int64(1)
|
||||||
|
tests := []struct {
|
||||||
|
opt api.PodLogOptions
|
||||||
|
errs int
|
||||||
|
}{
|
||||||
|
{api.PodLogOptions{}, 0},
|
||||||
|
{api.PodLogOptions{Previous: true}, 0},
|
||||||
|
{api.PodLogOptions{Follow: true}, 0},
|
||||||
|
{api.PodLogOptions{TailLines: &zero}, 0},
|
||||||
|
{api.PodLogOptions{TailLines: &negative}, 1},
|
||||||
|
{api.PodLogOptions{TailLines: &positive}, 0},
|
||||||
|
{api.PodLogOptions{LimitBytes: &zero}, 1},
|
||||||
|
{api.PodLogOptions{LimitBytes: &negative}, 1},
|
||||||
|
{api.PodLogOptions{LimitBytes: &positive}, 0},
|
||||||
|
{api.PodLogOptions{SinceSeconds: &negative}, 1},
|
||||||
|
{api.PodLogOptions{SinceSeconds: &positive}, 0},
|
||||||
|
{api.PodLogOptions{SinceSeconds: &zero}, 1},
|
||||||
|
{api.PodLogOptions{SinceTime: &now}, 0},
|
||||||
|
}
|
||||||
|
for i, test := range tests {
|
||||||
|
errs := ValidatePodLogOptions(&test.opt)
|
||||||
|
if test.errs != len(errs) {
|
||||||
|
t.Errorf("%d: Unexpected errors: %v", i, errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -583,7 +583,12 @@ func (c *Converter) defaultConvert(sv, dv reflect.Value, scope *scope) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
dv.Set(reflect.New(dt.Elem()))
|
dv.Set(reflect.New(dt.Elem()))
|
||||||
|
switch st.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Interface:
|
||||||
return c.convert(sv.Elem(), dv.Elem(), scope)
|
return c.convert(sv.Elem(), dv.Elem(), scope)
|
||||||
|
default:
|
||||||
|
return c.convert(sv, dv.Elem(), scope)
|
||||||
|
}
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
if sv.IsNil() {
|
if sv.IsNil() {
|
||||||
// Don't copy a nil ptr!
|
// Don't copy a nil ptr!
|
||||||
|
@ -39,6 +39,34 @@ func TestConverter_byteSlice(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConverter_MismatchedTypes(t *testing.T) {
|
||||||
|
c := NewConverter()
|
||||||
|
|
||||||
|
err := c.RegisterConversionFunc(
|
||||||
|
func(in *[]string, out *int, s Scope) error {
|
||||||
|
if str, err := strconv.Atoi((*in)[0]); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
*out = str
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
src := []string{"5"}
|
||||||
|
var dest *int
|
||||||
|
err = c.Convert(&src, &dest, 0, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if e, a := 5, *dest; e != a {
|
||||||
|
t.Errorf("expected %#v, got %#v", e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConverter_DefaultConvert(t *testing.T) {
|
func TestConverter_DefaultConvert(t *testing.T) {
|
||||||
type A struct {
|
type A struct {
|
||||||
Foo string
|
Foo string
|
||||||
|
@ -19,26 +19,35 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
|
||||||
"k8s.io/kubernetes/pkg/util/sets"
|
"k8s.io/kubernetes/pkg/util/sets"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
log_example = `# Return snapshot of ruby-container logs from pod 123456-7890.
|
log_example = `# Return snapshot logs from pod nginx with only one container
|
||||||
$ kubectl logs 123456-7890 ruby-container
|
$ kubectl logs nginx
|
||||||
|
|
||||||
# Return snapshot of previous terminated ruby-container logs from pod 123456-7890.
|
# Return snapshot of previous terminated ruby container logs from pod web-1
|
||||||
$ kubectl logs -p 123456-7890 ruby-container
|
$ kubectl logs -p -c ruby web-1
|
||||||
|
|
||||||
# Start streaming of ruby-container logs from pod 123456-7890.
|
# Begin streaming the logs of the ruby container in pod web-1
|
||||||
$ kubectl logs -f 123456-7890 ruby-container`
|
$ kubectl logs -f -c ruby web-1
|
||||||
|
|
||||||
|
# Display only the most recent 20 lines of output in pod nginx
|
||||||
|
$ kubectl logs --tail=20 nginx
|
||||||
|
|
||||||
|
# Show all logs from pod nginx written in the last hour
|
||||||
|
$ kubectl logs --since=1h nginx`
|
||||||
)
|
)
|
||||||
|
|
||||||
func selectContainer(pod *api.Pod, in io.Reader, out io.Writer) string {
|
func selectContainer(pod *api.Pod, in io.Reader, out io.Writer) string {
|
||||||
@ -82,8 +91,13 @@ func NewCmdLog(f *cmdutil.Factory, out io.Writer) *cobra.Command {
|
|||||||
Aliases: []string{"log"},
|
Aliases: []string{"log"},
|
||||||
}
|
}
|
||||||
cmd.Flags().BoolP("follow", "f", false, "Specify if the logs should be streamed.")
|
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().Bool("interactive", true, "If true, prompt the user for input when required. Default true.")
|
cmd.Flags().Bool("interactive", true, "If true, prompt the user for input when required. Default true.")
|
||||||
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().BoolP("previous", "p", false, "If true, print the logs for the previous instance of the container in a pod if it exists.")
|
||||||
|
cmd.Flags().Int("limit-bytes", 0, "Maximum bytes of logs to return. Defaults to no limit.")
|
||||||
|
cmd.Flags().Int("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().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().StringVarP(¶ms.containerName, "container", "c", "", "Container name")
|
cmd.Flags().StringVarP(¶ms.containerName, "container", "c", "", "Container name")
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@ -102,6 +116,12 @@ func RunLog(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string
|
|||||||
return cmdutil.UsageError(cmd, "log POD [CONTAINER]")
|
return cmdutil.UsageError(cmd, "log POD [CONTAINER]")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sinceSeconds := cmdutil.GetFlagDuration(cmd, "since")
|
||||||
|
sinceTime := cmdutil.GetFlagString(cmd, "since-time")
|
||||||
|
if len(sinceTime) > 0 && sinceSeconds > 0 {
|
||||||
|
return cmdutil.UsageError(cmd, "only one of --since, --since-time may be specified")
|
||||||
|
}
|
||||||
|
|
||||||
namespace, _, err := f.DefaultNamespace()
|
namespace, _, err := f.DefaultNamespace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -137,28 +157,57 @@ func RunLog(f *cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
follow := false
|
logOptions := &api.PodLogOptions{
|
||||||
if cmdutil.GetFlagBool(cmd, "follow") {
|
Container: container,
|
||||||
follow = true
|
Follow: cmdutil.GetFlagBool(cmd, "follow"),
|
||||||
|
Previous: cmdutil.GetFlagBool(cmd, "previous"),
|
||||||
|
Timestamps: cmdutil.GetFlagBool(cmd, "timestamps"),
|
||||||
|
}
|
||||||
|
if sinceSeconds > 0 {
|
||||||
|
// round up to the nearest second
|
||||||
|
sec := int64(math.Ceil(float64(sinceSeconds) / float64(time.Second)))
|
||||||
|
logOptions.SinceSeconds = &sec
|
||||||
|
}
|
||||||
|
if t, err := api.ParseRFC3339(sinceTime, unversioned.Now); err == nil {
|
||||||
|
logOptions.SinceTime = &t
|
||||||
|
}
|
||||||
|
if limitBytes := cmdutil.GetFlagInt(cmd, "limit-bytes"); limitBytes != 0 {
|
||||||
|
i := int64(limitBytes)
|
||||||
|
logOptions.LimitBytes = &i
|
||||||
|
}
|
||||||
|
if tail := cmdutil.GetFlagInt(cmd, "tail"); tail >= 0 {
|
||||||
|
i := int64(tail)
|
||||||
|
logOptions.TailLines = &i
|
||||||
}
|
}
|
||||||
|
|
||||||
previous := false
|
return handleLog(client, namespace, podID, logOptions, out)
|
||||||
if cmdutil.GetFlagBool(cmd, "previous") {
|
|
||||||
previous = true
|
|
||||||
}
|
|
||||||
return handleLog(client, namespace, podID, container, follow, previous, out)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleLog(client *client.Client, namespace, podID, container string, follow, previous bool, out io.Writer) error {
|
func handleLog(client *client.Client, namespace, podID string, logOptions *api.PodLogOptions, out io.Writer) error {
|
||||||
readCloser, err := client.RESTClient.Get().
|
// TODO: transform this into a PodLogOptions call
|
||||||
|
req := client.RESTClient.Get().
|
||||||
Namespace(namespace).
|
Namespace(namespace).
|
||||||
Name(podID).
|
Name(podID).
|
||||||
Resource("pods").
|
Resource("pods").
|
||||||
SubResource("log").
|
SubResource("log").
|
||||||
Param("follow", strconv.FormatBool(follow)).
|
Param("follow", strconv.FormatBool(logOptions.Follow)).
|
||||||
Param("container", container).
|
Param("container", logOptions.Container).
|
||||||
Param("previous", strconv.FormatBool(previous)).
|
Param("previous", strconv.FormatBool(logOptions.Previous)).
|
||||||
Stream()
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -291,7 +291,7 @@ func handleAttachPod(c *client.Client, pod *api.Pod, opts *AttachOptions) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if status == api.PodSucceeded || status == api.PodFailed {
|
if status == api.PodSucceeded || status == api.PodFailed {
|
||||||
return handleLog(c, pod.Namespace, pod.Name, pod.Spec.Containers[0].Name, false, false, opts.Out)
|
return handleLog(c, pod.Namespace, pod.Name, &api.PodLogOptions{Container: pod.Spec.Containers[0].Name}, opts.Out)
|
||||||
}
|
}
|
||||||
opts.Client = c
|
opts.Client = c
|
||||||
opts.PodName = pod.Name
|
opts.PodName = pod.Name
|
||||||
|
@ -260,6 +260,15 @@ func GetFlagInt(cmd *cobra.Command, flag string) int {
|
|||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assumes the flag has a default value.
|
||||||
|
func GetFlagInt64(cmd *cobra.Command, flag string) int64 {
|
||||||
|
i, err := cmd.Flags().GetInt64(flag)
|
||||||
|
if err != nil {
|
||||||
|
glog.Fatalf("err accessing flag %s for command %s: %v", flag, cmd.Name(), err)
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
func GetFlagDuration(cmd *cobra.Command, flag string) time.Duration {
|
func GetFlagDuration(cmd *cobra.Command, flag string) time.Duration {
|
||||||
d, err := cmd.Flags().GetDuration(flag)
|
d, err := cmd.Flags().GetDuration(flag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -251,7 +251,7 @@ func (f *FakeRuntime) RunInContainer(containerID string, cmd []string) ([]byte,
|
|||||||
return []byte{}, f.Err
|
return []byte{}, f.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FakeRuntime) GetContainerLogs(pod *api.Pod, containerID, tail string, follow bool, stdout, stderr io.Writer) (err error) {
|
func (f *FakeRuntime) GetContainerLogs(pod *api.Pod, containerID string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) (err error) {
|
||||||
f.Lock()
|
f.Lock()
|
||||||
defer f.Unlock()
|
defer f.Unlock()
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ type Runtime interface {
|
|||||||
// default, it returns a snapshot of the container log. Set 'follow' to true to
|
// default, it returns a snapshot of the container log. Set 'follow' to true to
|
||||||
// stream the log. Set 'follow' to false and specify the number of lines (e.g.
|
// stream the log. Set 'follow' to false and specify the number of lines (e.g.
|
||||||
// "100" or "all") to tail the log.
|
// "100" or "all") to tail the log.
|
||||||
GetContainerLogs(pod *api.Pod, containerID, tail string, follow bool, stdout, stderr io.Writer) (err error)
|
GetContainerLogs(pod *api.Pod, containerID string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) (err error)
|
||||||
// ContainerCommandRunner encapsulates the command runner interfaces for testability.
|
// ContainerCommandRunner encapsulates the command runner interfaces for testability.
|
||||||
ContainerCommandRunner
|
ContainerCommandRunner
|
||||||
// ContainerAttach encapsulates the attaching to containers for testability
|
// ContainerAttach encapsulates the attaching to containers for testability
|
||||||
|
@ -259,20 +259,29 @@ func (sc *reasonInfoCache) Get(uid types.UID, name string) (reasonInfo, bool) {
|
|||||||
// stream the log. Set 'follow' to false and specify the number of lines (e.g.
|
// stream the log. Set 'follow' to false and specify the number of lines (e.g.
|
||||||
// "100" or "all") to tail the log.
|
// "100" or "all") to tail the log.
|
||||||
// TODO: Make 'RawTerminal' option flagable.
|
// TODO: Make 'RawTerminal' option flagable.
|
||||||
func (dm *DockerManager) GetContainerLogs(pod *api.Pod, containerID, tail string, follow bool, stdout, stderr io.Writer) (err error) {
|
func (dm *DockerManager) GetContainerLogs(pod *api.Pod, containerID string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) (err error) {
|
||||||
|
var since int64
|
||||||
|
if logOptions.SinceSeconds != nil {
|
||||||
|
t := unversioned.Now().Add(-time.Duration(*logOptions.SinceSeconds) * time.Second)
|
||||||
|
since = t.Unix()
|
||||||
|
}
|
||||||
|
if logOptions.SinceTime != nil {
|
||||||
|
since = logOptions.SinceTime.Unix()
|
||||||
|
}
|
||||||
opts := docker.LogsOptions{
|
opts := docker.LogsOptions{
|
||||||
Container: containerID,
|
Container: containerID,
|
||||||
Stdout: true,
|
Stdout: true,
|
||||||
Stderr: true,
|
Stderr: true,
|
||||||
OutputStream: stdout,
|
OutputStream: stdout,
|
||||||
ErrorStream: stderr,
|
ErrorStream: stderr,
|
||||||
Timestamps: false,
|
Timestamps: logOptions.Timestamps,
|
||||||
|
Since: since,
|
||||||
|
Follow: logOptions.Follow,
|
||||||
RawTerminal: false,
|
RawTerminal: false,
|
||||||
Follow: follow,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !follow {
|
if !logOptions.Follow && logOptions.TailLines != nil {
|
||||||
opts.Tail = tail
|
opts.Tail = strconv.FormatInt(*logOptions.TailLines, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = dm.client.Logs(opts)
|
err = dm.client.Logs(opts)
|
||||||
|
@ -1916,7 +1916,7 @@ func (kl *Kubelet) validateContainerStatus(podStatus *api.PodStatus, containerNa
|
|||||||
// GetKubeletContainerLogs returns logs from the container
|
// GetKubeletContainerLogs returns logs from the container
|
||||||
// TODO: this method is returning logs of random container attempts, when it should be returning the most recent attempt
|
// TODO: this method is returning logs of random container attempts, when it should be returning the most recent attempt
|
||||||
// or all of them.
|
// or all of them.
|
||||||
func (kl *Kubelet) GetKubeletContainerLogs(podFullName, containerName, tail string, follow, previous bool, stdout, stderr io.Writer) error {
|
func (kl *Kubelet) GetKubeletContainerLogs(podFullName, containerName string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error {
|
||||||
// TODO(vmarmol): Refactor to not need the pod status and verification.
|
// TODO(vmarmol): Refactor to not need the pod status and verification.
|
||||||
// Pod workers periodically write status to statusManager. If status is not
|
// Pod workers periodically write status to statusManager. If status is not
|
||||||
// cached there, something is wrong (or kubelet just restarted and hasn't
|
// cached there, something is wrong (or kubelet just restarted and hasn't
|
||||||
@ -1940,13 +1940,13 @@ func (kl *Kubelet) GetKubeletContainerLogs(podFullName, containerName, tail stri
|
|||||||
// No log is available if pod is not in a "known" phase (e.g. Unknown).
|
// No log is available if pod is not in a "known" phase (e.g. Unknown).
|
||||||
return fmt.Errorf("Pod %q in namespace %q : %v", name, namespace, err)
|
return fmt.Errorf("Pod %q in namespace %q : %v", name, namespace, err)
|
||||||
}
|
}
|
||||||
containerID, err := kl.validateContainerStatus(&podStatus, containerName, previous)
|
containerID, err := kl.validateContainerStatus(&podStatus, containerName, logOptions.Previous)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// No log is available if the container status is missing or is in the
|
// No log is available if the container status is missing or is in the
|
||||||
// waiting state.
|
// waiting state.
|
||||||
return fmt.Errorf("Pod %q in namespace %q: %v", name, namespace, err)
|
return fmt.Errorf("Pod %q in namespace %q: %v", name, namespace, err)
|
||||||
}
|
}
|
||||||
return kl.containerRuntime.GetContainerLogs(pod, containerID, tail, follow, stdout, stderr)
|
return kl.containerRuntime.GetContainerLogs(pod, containerID, logOptions, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHostname Returns the hostname as the kubelet sees it.
|
// GetHostname Returns the hostname as the kubelet sees it.
|
||||||
|
@ -1055,23 +1055,20 @@ func (r *runtime) SyncPod(pod *api.Pod, runningPod kubecontainer.Pod, podStatus
|
|||||||
// See https://github.com/coreos/rkt/blob/master/Documentation/commands.md#logging for more details.
|
// See https://github.com/coreos/rkt/blob/master/Documentation/commands.md#logging for more details.
|
||||||
//
|
//
|
||||||
// TODO(yifan): If the rkt is using lkvm as the stage1 image, then this function will fail.
|
// TODO(yifan): If the rkt is using lkvm as the stage1 image, then this function will fail.
|
||||||
func (r *runtime) GetContainerLogs(pod *api.Pod, containerID string, tail string, follow bool, stdout, stderr io.Writer) error {
|
func (r *runtime) GetContainerLogs(pod *api.Pod, containerID string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error {
|
||||||
id, err := parseContainerID(containerID)
|
id, err := parseContainerID(containerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("journalctl", "-M", fmt.Sprintf("rkt-%s", id.uuid), "-u", id.appName)
|
cmd := exec.Command("journalctl", "-M", fmt.Sprintf("rkt-%s", id.uuid), "-u", id.appName)
|
||||||
if follow {
|
if logOptions.Follow {
|
||||||
cmd.Args = append(cmd.Args, "-f")
|
cmd.Args = append(cmd.Args, "-f")
|
||||||
}
|
}
|
||||||
if tail == "all" {
|
if logOptions.TailLines == nil {
|
||||||
cmd.Args = append(cmd.Args, "-a")
|
cmd.Args = append(cmd.Args, "-a")
|
||||||
} else {
|
} else {
|
||||||
_, err := strconv.Atoi(tail)
|
cmd.Args = append(cmd.Args, "-n", strconv.FormatInt(*logOptions.TailLines, 10))
|
||||||
if err == nil {
|
|
||||||
cmd.Args = append(cmd.Args, "-n", tail)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cmd.Stdout, cmd.Stderr = stdout, stderr
|
cmd.Stdout, cmd.Stderr = stdout, stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
|
@ -36,7 +36,11 @@ import (
|
|||||||
cadvisorApi "github.com/google/cadvisor/info/v1"
|
cadvisorApi "github.com/google/cadvisor/info/v1"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
apierrs "k8s.io/kubernetes/pkg/api/errors"
|
||||||
"k8s.io/kubernetes/pkg/api/latest"
|
"k8s.io/kubernetes/pkg/api/latest"
|
||||||
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/api/v1"
|
||||||
|
"k8s.io/kubernetes/pkg/api/validation"
|
||||||
"k8s.io/kubernetes/pkg/healthz"
|
"k8s.io/kubernetes/pkg/healthz"
|
||||||
"k8s.io/kubernetes/pkg/httplog"
|
"k8s.io/kubernetes/pkg/httplog"
|
||||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||||
@ -44,6 +48,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/util/flushwriter"
|
"k8s.io/kubernetes/pkg/util/flushwriter"
|
||||||
"k8s.io/kubernetes/pkg/util/httpstream"
|
"k8s.io/kubernetes/pkg/util/httpstream"
|
||||||
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
|
"k8s.io/kubernetes/pkg/util/httpstream/spdy"
|
||||||
|
"k8s.io/kubernetes/pkg/util/limitwriter"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is a http.Handler which exposes kubelet functionality over HTTP.
|
// Server is a http.Handler which exposes kubelet functionality over HTTP.
|
||||||
@ -102,7 +107,7 @@ type HostInterface interface {
|
|||||||
RunInContainer(name string, uid types.UID, container string, cmd []string) ([]byte, error)
|
RunInContainer(name string, uid types.UID, container string, cmd []string) ([]byte, error)
|
||||||
ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
ExecInContainer(name string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
||||||
AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
AttachContainer(name string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
||||||
GetKubeletContainerLogs(podFullName, containerName, tail string, follow, previous bool, stdout, stderr io.Writer) error
|
GetKubeletContainerLogs(podFullName, containerName string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error
|
||||||
ServeLogs(w http.ResponseWriter, req *http.Request)
|
ServeLogs(w http.ResponseWriter, req *http.Request)
|
||||||
PortForward(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
PortForward(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
||||||
StreamingConnectionIdleTimeout() time.Duration
|
StreamingConnectionIdleTimeout() time.Duration
|
||||||
@ -308,6 +313,7 @@ func (s *Server) getContainerLogs(request *restful.Request, response *restful.Re
|
|||||||
|
|
||||||
if len(podID) == 0 {
|
if len(podID) == 0 {
|
||||||
// TODO: Why return JSON when the rest return plaintext errors?
|
// TODO: Why return JSON when the rest return plaintext errors?
|
||||||
|
// TODO: Why return plaintext errors?
|
||||||
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing podID."}`))
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Missing podID."}`))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -322,9 +328,32 @@ func (s *Server) getContainerLogs(request *restful.Request, response *restful.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
follow, _ := strconv.ParseBool(request.QueryParameter("follow"))
|
query := request.Request.URL.Query()
|
||||||
previous, _ := strconv.ParseBool(request.QueryParameter("previous"))
|
// backwards compatibility for the "tail" query parameter
|
||||||
tail := request.QueryParameter("tail")
|
if tail := request.QueryParameter("tail"); len(tail) > 0 {
|
||||||
|
query["tailLines"] = []string{tail}
|
||||||
|
// "all" is the same as omitting tail
|
||||||
|
if tail == "all" {
|
||||||
|
delete(query, "tailLines")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// container logs on the kubelet are locked to v1
|
||||||
|
versioned := &v1.PodLogOptions{}
|
||||||
|
if err := api.Scheme.Convert(&query, versioned); err != nil {
|
||||||
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Unable to decode query."}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := api.Scheme.ConvertToVersion(versioned, "")
|
||||||
|
if err != nil {
|
||||||
|
response.WriteError(http.StatusBadRequest, fmt.Errorf(`{"message": "Unable to convert request query."}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logOptions := out.(*api.PodLogOptions)
|
||||||
|
logOptions.TypeMeta = unversioned.TypeMeta{}
|
||||||
|
if errs := validation.ValidatePodLogOptions(logOptions); len(errs) > 0 {
|
||||||
|
response.WriteError(apierrs.StatusUnprocessableEntity, fmt.Errorf(`{"message": "Invalid request."}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
pod, ok := s.host.GetPodByName(podNamespace, podID)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -348,11 +377,15 @@ func (s *Server) getContainerLogs(request *restful.Request, response *restful.Re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fw := flushwriter.Wrap(response.ResponseWriter)
|
fw := flushwriter.Wrap(response.ResponseWriter)
|
||||||
|
if logOptions.LimitBytes != nil {
|
||||||
|
fw = limitwriter.New(fw, *logOptions.LimitBytes)
|
||||||
|
}
|
||||||
response.Header().Set("Transfer-Encoding", "chunked")
|
response.Header().Set("Transfer-Encoding", "chunked")
|
||||||
response.WriteHeader(http.StatusOK)
|
response.WriteHeader(http.StatusOK)
|
||||||
err := s.host.GetKubeletContainerLogs(kubecontainer.GetPodFullName(pod), containerName, tail, follow, previous, fw, fw)
|
if err := s.host.GetKubeletContainerLogs(kubecontainer.GetPodFullName(pod), containerName, logOptions, fw, fw); err != nil {
|
||||||
if err != nil {
|
if err != limitwriter.ErrMaximumWrite {
|
||||||
response.WriteError(http.StatusInternalServerError, err)
|
response.WriteError(http.StatusInternalServerError, err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
|
|
||||||
cadvisorApi "github.com/google/cadvisor/info/v1"
|
cadvisorApi "github.com/google/cadvisor/info/v1"
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
|
apierrs "k8s.io/kubernetes/pkg/api/errors"
|
||||||
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
kubecontainer "k8s.io/kubernetes/pkg/kubelet/container"
|
||||||
"k8s.io/kubernetes/pkg/kubelet/dockertools"
|
"k8s.io/kubernetes/pkg/kubelet/dockertools"
|
||||||
"k8s.io/kubernetes/pkg/types"
|
"k8s.io/kubernetes/pkg/types"
|
||||||
@ -54,7 +55,7 @@ type fakeKubelet struct {
|
|||||||
execFunc func(pod string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
execFunc func(pod string, uid types.UID, container string, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
||||||
attachFunc func(pod string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
attachFunc func(pod string, uid types.UID, container string, in io.Reader, out, err io.WriteCloser, tty bool) error
|
||||||
portForwardFunc func(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
portForwardFunc func(name string, uid types.UID, port uint16, stream io.ReadWriteCloser) error
|
||||||
containerLogsFunc func(podFullName, containerName, tail string, follow, pervious bool, stdout, stderr io.Writer) error
|
containerLogsFunc func(podFullName, containerName string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error
|
||||||
streamingConnectionIdleTimeoutFunc func() time.Duration
|
streamingConnectionIdleTimeoutFunc func() time.Duration
|
||||||
hostnameFunc func() string
|
hostnameFunc func() string
|
||||||
resyncInterval time.Duration
|
resyncInterval time.Duration
|
||||||
@ -101,8 +102,8 @@ func (fk *fakeKubelet) ServeLogs(w http.ResponseWriter, req *http.Request) {
|
|||||||
fk.logFunc(w, req)
|
fk.logFunc(w, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fk *fakeKubelet) GetKubeletContainerLogs(podFullName, containerName, tail string, follow, previous bool, stdout, stderr io.Writer) error {
|
func (fk *fakeKubelet) GetKubeletContainerLogs(podFullName, containerName string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error {
|
||||||
return fk.containerLogsFunc(podFullName, containerName, tail, follow, previous, stdout, stderr)
|
return fk.containerLogsFunc(podFullName, containerName, logOptions, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fk *fakeKubelet) GetHostname() string {
|
func (fk *fakeKubelet) GetHostname() string {
|
||||||
@ -558,22 +559,16 @@ func setPodByNameFunc(fw *serverTestFramework, namespace, pod, container string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setGetContainerLogsFunc(fw *serverTestFramework, t *testing.T, expectedPodName, expectedContainerName, expectedTail string, expectedFollow, expectedPrevious bool, output string) {
|
func setGetContainerLogsFunc(fw *serverTestFramework, t *testing.T, expectedPodName, expectedContainerName string, expectedLogOptions *api.PodLogOptions, output string) {
|
||||||
fw.fakeKubelet.containerLogsFunc = func(podFullName, containerName, tail string, follow, previous bool, stdout, stderr io.Writer) error {
|
fw.fakeKubelet.containerLogsFunc = func(podFullName, containerName string, logOptions *api.PodLogOptions, stdout, stderr io.Writer) error {
|
||||||
if podFullName != expectedPodName {
|
if podFullName != expectedPodName {
|
||||||
t.Errorf("expected %s, got %s", expectedPodName, podFullName)
|
t.Errorf("expected %s, got %s", expectedPodName, podFullName)
|
||||||
}
|
}
|
||||||
if containerName != expectedContainerName {
|
if containerName != expectedContainerName {
|
||||||
t.Errorf("expected %s, got %s", expectedContainerName, containerName)
|
t.Errorf("expected %s, got %s", expectedContainerName, containerName)
|
||||||
}
|
}
|
||||||
if tail != expectedTail {
|
if !reflect.DeepEqual(expectedLogOptions, logOptions) {
|
||||||
t.Errorf("expected %s, got %s", expectedTail, tail)
|
t.Errorf("expected %#v, got %#v", expectedLogOptions, logOptions)
|
||||||
}
|
|
||||||
if follow != expectedFollow {
|
|
||||||
t.Errorf("expected %t, got %t", expectedFollow, follow)
|
|
||||||
}
|
|
||||||
if previous != expectedPrevious {
|
|
||||||
t.Errorf("expected %t, got %t", expectedPrevious, previous)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
io.WriteString(stdout, output)
|
io.WriteString(stdout, output)
|
||||||
@ -581,6 +576,7 @@ func setGetContainerLogsFunc(fw *serverTestFramework, t *testing.T, expectedPodN
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: I really want to be a table driven test
|
||||||
func TestContainerLogs(t *testing.T) {
|
func TestContainerLogs(t *testing.T) {
|
||||||
fw := newServerTest()
|
fw := newServerTest()
|
||||||
output := "foo bar"
|
output := "foo bar"
|
||||||
@ -588,11 +584,8 @@ func TestContainerLogs(t *testing.T) {
|
|||||||
podName := "foo"
|
podName := "foo"
|
||||||
expectedPodName := getPodName(podName, podNamespace)
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
expectedContainerName := "baz"
|
expectedContainerName := "baz"
|
||||||
expectedTail := ""
|
|
||||||
expectedFollow := false
|
|
||||||
expectedPrevious := false
|
|
||||||
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, expectedTail, expectedFollow, expectedPrevious, output)
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{}, output)
|
||||||
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName)
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Got error GETing: %v", err)
|
t.Errorf("Got error GETing: %v", err)
|
||||||
@ -609,6 +602,32 @@ func TestContainerLogs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerLogsWithLimitBytes(t *testing.T) {
|
||||||
|
fw := newServerTest()
|
||||||
|
output := "foo bar"
|
||||||
|
podNamespace := "other"
|
||||||
|
podName := "foo"
|
||||||
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
|
expectedContainerName := "baz"
|
||||||
|
bytes := int64(3)
|
||||||
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{LimitBytes: &bytes}, output)
|
||||||
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?limitBytes=3")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got error GETing: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error reading container logs: %v", err)
|
||||||
|
}
|
||||||
|
result := string(body)
|
||||||
|
if result != output[:bytes] {
|
||||||
|
t.Errorf("Expected: '%v', got: '%v'", output[:bytes], result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContainerLogsWithTail(t *testing.T) {
|
func TestContainerLogsWithTail(t *testing.T) {
|
||||||
fw := newServerTest()
|
fw := newServerTest()
|
||||||
output := "foo bar"
|
output := "foo bar"
|
||||||
@ -616,11 +635,35 @@ func TestContainerLogsWithTail(t *testing.T) {
|
|||||||
podName := "foo"
|
podName := "foo"
|
||||||
expectedPodName := getPodName(podName, podNamespace)
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
expectedContainerName := "baz"
|
expectedContainerName := "baz"
|
||||||
expectedTail := "5"
|
expectedTail := int64(5)
|
||||||
expectedFollow := false
|
|
||||||
expectedPrevious := false
|
|
||||||
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, expectedTail, expectedFollow, expectedPrevious, output)
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{TailLines: &expectedTail}, output)
|
||||||
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tailLines=5")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got error GETing: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error reading container logs: %v", err)
|
||||||
|
}
|
||||||
|
result := string(body)
|
||||||
|
if result != output {
|
||||||
|
t.Errorf("Expected: '%v', got: '%v'", output, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerLogsWithLegacyTail(t *testing.T) {
|
||||||
|
fw := newServerTest()
|
||||||
|
output := "foo bar"
|
||||||
|
podNamespace := "other"
|
||||||
|
podName := "foo"
|
||||||
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
|
expectedContainerName := "baz"
|
||||||
|
expectedTail := int64(5)
|
||||||
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{TailLines: &expectedTail}, output)
|
||||||
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tail=5")
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tail=5")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Got error GETing: %v", err)
|
t.Errorf("Got error GETing: %v", err)
|
||||||
@ -637,6 +680,50 @@ func TestContainerLogsWithTail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContainerLogsWithTailAll(t *testing.T) {
|
||||||
|
fw := newServerTest()
|
||||||
|
output := "foo bar"
|
||||||
|
podNamespace := "other"
|
||||||
|
podName := "foo"
|
||||||
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
|
expectedContainerName := "baz"
|
||||||
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{}, output)
|
||||||
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tail=all")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got error GETing: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error reading container logs: %v", err)
|
||||||
|
}
|
||||||
|
result := string(body)
|
||||||
|
if result != output {
|
||||||
|
t.Errorf("Expected: '%v', got: '%v'", output, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContainerLogsWithInvalidTail(t *testing.T) {
|
||||||
|
fw := newServerTest()
|
||||||
|
output := "foo bar"
|
||||||
|
podNamespace := "other"
|
||||||
|
podName := "foo"
|
||||||
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
|
expectedContainerName := "baz"
|
||||||
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{}, output)
|
||||||
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?tail=-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Got error GETing: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != apierrs.StatusUnprocessableEntity {
|
||||||
|
t.Errorf("Unexpected non-error reading container logs: %#v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContainerLogsWithFollow(t *testing.T) {
|
func TestContainerLogsWithFollow(t *testing.T) {
|
||||||
fw := newServerTest()
|
fw := newServerTest()
|
||||||
output := "foo bar"
|
output := "foo bar"
|
||||||
@ -644,11 +731,8 @@ func TestContainerLogsWithFollow(t *testing.T) {
|
|||||||
podName := "foo"
|
podName := "foo"
|
||||||
expectedPodName := getPodName(podName, podNamespace)
|
expectedPodName := getPodName(podName, podNamespace)
|
||||||
expectedContainerName := "baz"
|
expectedContainerName := "baz"
|
||||||
expectedTail := ""
|
|
||||||
expectedFollow := true
|
|
||||||
expectedPrevious := false
|
|
||||||
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
setPodByNameFunc(fw, podNamespace, podName, expectedContainerName)
|
||||||
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, expectedTail, expectedFollow, expectedPrevious, output)
|
setGetContainerLogsFunc(fw, t, expectedPodName, expectedContainerName, &api.PodLogOptions{Follow: true}, output)
|
||||||
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?follow=1")
|
resp, err := http.Get(fw.testHTTPServer.URL + "/containerLogs/" + podNamespace + "/" + podName + "/" + expectedContainerName + "?follow=1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Got error GETing: %v", err)
|
t.Errorf("Got error GETing: %v", err)
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
etcderr "k8s.io/kubernetes/pkg/api/errors/etcd"
|
etcderr "k8s.io/kubernetes/pkg/api/errors/etcd"
|
||||||
"k8s.io/kubernetes/pkg/api/rest"
|
"k8s.io/kubernetes/pkg/api/rest"
|
||||||
"k8s.io/kubernetes/pkg/api/unversioned"
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
||||||
|
"k8s.io/kubernetes/pkg/api/validation"
|
||||||
"k8s.io/kubernetes/pkg/capabilities"
|
"k8s.io/kubernetes/pkg/capabilities"
|
||||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||||
"k8s.io/kubernetes/pkg/fields"
|
"k8s.io/kubernetes/pkg/fields"
|
||||||
@ -211,6 +212,7 @@ func (r *StatusREST) Update(ctx api.Context, obj runtime.Object) (runtime.Object
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LogREST implements the log endpoint for a Pod
|
// LogREST implements the log endpoint for a Pod
|
||||||
|
// TODO: move me into pod/rest - I'm generic to store type via ResourceGetter
|
||||||
type LogREST struct {
|
type LogREST struct {
|
||||||
store *etcdgeneric.Etcd
|
store *etcdgeneric.Etcd
|
||||||
kubeletConn client.ConnectionInfoGetter
|
kubeletConn client.ConnectionInfoGetter
|
||||||
@ -231,6 +233,9 @@ func (r *LogREST) Get(ctx api.Context, name string, opts runtime.Object) (runtim
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("Invalid options object: %#v", opts)
|
return nil, fmt.Errorf("Invalid options object: %#v", opts)
|
||||||
}
|
}
|
||||||
|
if errs := validation.ValidatePodLogOptions(logOpts); len(errs) > 0 {
|
||||||
|
return nil, errors.NewInvalid("podlogs", name, errs)
|
||||||
|
}
|
||||||
location, transport, err := pod.LogLocation(r.store, r.kubeletConn, ctx, name, logOpts)
|
location, transport, err := pod.LogLocation(r.store, r.kubeletConn, ctx, name, logOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -249,6 +254,7 @@ func (r *LogREST) NewGetOptions() (runtime.Object, bool, string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ProxyREST implements the proxy subresource for a Pod
|
// ProxyREST implements the proxy subresource for a Pod
|
||||||
|
// TODO: move me into pod/rest - I'm generic to store type via ResourceGetter
|
||||||
type ProxyREST struct {
|
type ProxyREST struct {
|
||||||
store *etcdgeneric.Etcd
|
store *etcdgeneric.Etcd
|
||||||
}
|
}
|
||||||
@ -291,6 +297,7 @@ func (r *ProxyREST) Connect(ctx api.Context, id string, opts runtime.Object) (re
|
|||||||
var upgradeableMethods = []string{"GET", "POST"}
|
var upgradeableMethods = []string{"GET", "POST"}
|
||||||
|
|
||||||
// AttachREST implements the attach subresource for a Pod
|
// AttachREST implements the attach subresource for a Pod
|
||||||
|
// TODO: move me into pod/rest - I'm generic to store type via ResourceGetter
|
||||||
type AttachREST struct {
|
type AttachREST struct {
|
||||||
store *etcdgeneric.Etcd
|
store *etcdgeneric.Etcd
|
||||||
kubeletConn client.ConnectionInfoGetter
|
kubeletConn client.ConnectionInfoGetter
|
||||||
@ -328,6 +335,7 @@ func (r *AttachREST) ConnectMethods() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecREST implements the exec subresource for a Pod
|
// ExecREST implements the exec subresource for a Pod
|
||||||
|
// TODO: move me into pod/rest - I'm generic to store type via ResourceGetter
|
||||||
type ExecREST struct {
|
type ExecREST struct {
|
||||||
store *etcdgeneric.Etcd
|
store *etcdgeneric.Etcd
|
||||||
kubeletConn client.ConnectionInfoGetter
|
kubeletConn client.ConnectionInfoGetter
|
||||||
@ -365,6 +373,7 @@ func (r *ExecREST) ConnectMethods() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PortForwardREST implements the portforward subresource for a Pod
|
// PortForwardREST implements the portforward subresource for a Pod
|
||||||
|
// TODO: move me into pod/rest - I'm generic to store type via ResourceGetter
|
||||||
type PortForwardREST struct {
|
type PortForwardREST struct {
|
||||||
store *etcdgeneric.Etcd
|
store *etcdgeneric.Etcd
|
||||||
kubeletConn client.ConnectionInfoGetter
|
kubeletConn client.ConnectionInfoGetter
|
||||||
|
@ -734,3 +734,21 @@ func TestEtcdUpdateStatus(t *testing.T) {
|
|||||||
t.Errorf("unexpected object: %s", util.ObjectDiff(&expected, podOut))
|
t.Errorf("unexpected object: %s", util.ObjectDiff(&expected, podOut))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPodLogValidates(t *testing.T) {
|
||||||
|
etcdStorage, _ := registrytest.NewEtcdStorage(t, "")
|
||||||
|
storage := NewStorage(etcdStorage, false, nil)
|
||||||
|
|
||||||
|
negativeOne := int64(-1)
|
||||||
|
testCases := []*api.PodLogOptions{
|
||||||
|
{SinceSeconds: &negativeOne},
|
||||||
|
{TailLines: &negativeOne},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
_, err := storage.Log.Get(api.NewDefaultContext(), "test", tc)
|
||||||
|
if !errors.IsInvalid(err) {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/api"
|
"k8s.io/kubernetes/pkg/api"
|
||||||
"k8s.io/kubernetes/pkg/api/errors"
|
"k8s.io/kubernetes/pkg/api/errors"
|
||||||
@ -253,6 +255,21 @@ func LogLocation(getter ResourceGetter, connInfo client.ConnectionInfoGetter, ct
|
|||||||
if opts.Previous {
|
if opts.Previous {
|
||||||
params.Add("previous", "true")
|
params.Add("previous", "true")
|
||||||
}
|
}
|
||||||
|
if opts.Timestamps {
|
||||||
|
params.Add("timestamps", "true")
|
||||||
|
}
|
||||||
|
if opts.SinceSeconds != nil {
|
||||||
|
params.Add("sinceSeconds", strconv.FormatInt(*opts.SinceSeconds, 10))
|
||||||
|
}
|
||||||
|
if opts.SinceTime != nil {
|
||||||
|
params.Add("sinceTime", opts.SinceTime.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
if opts.TailLines != nil {
|
||||||
|
params.Add("tailLines", strconv.FormatInt(*opts.TailLines, 10))
|
||||||
|
}
|
||||||
|
if opts.LimitBytes != nil {
|
||||||
|
params.Add("limitBytes", strconv.FormatInt(*opts.LimitBytes, 10))
|
||||||
|
}
|
||||||
loc := &url.URL{
|
loc := &url.URL{
|
||||||
Scheme: nodeScheme,
|
Scheme: nodeScheme,
|
||||||
Host: fmt.Sprintf("%s:%d", nodeHost, nodePort),
|
Host: fmt.Sprintf("%s:%d", nodeHost, nodePort),
|
||||||
|
19
pkg/util/limitwriter/doc.go
Normal file
19
pkg/util/limitwriter/doc.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 limitwriter provides a writer that only allows a certain number of bytes to be
|
||||||
|
// written.
|
||||||
|
package limitwriter
|
53
pkg/util/limitwriter/limitwriter.go
Normal file
53
pkg/util/limitwriter/limitwriter.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 limitwriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New creates a writer that is limited to writing at most n bytes to w. This writer is not
|
||||||
|
// thread safe.
|
||||||
|
func New(w io.Writer, n int64) io.Writer {
|
||||||
|
return &limitWriter{
|
||||||
|
w: w,
|
||||||
|
n: n,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrMaximumWrite is returned when all bytes have been written.
|
||||||
|
var ErrMaximumWrite = errors.New("maximum write")
|
||||||
|
|
||||||
|
type limitWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
n int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *limitWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if int64(len(p)) > w.n {
|
||||||
|
p = p[:w.n]
|
||||||
|
}
|
||||||
|
if len(p) > 0 {
|
||||||
|
n, err = w.w.Write(p)
|
||||||
|
w.n -= int64(n)
|
||||||
|
}
|
||||||
|
if w.n == 0 {
|
||||||
|
err = ErrMaximumWrite
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
@ -464,18 +464,57 @@ var _ = Describe("Kubectl client", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Describe("Kubectl logs", func() {
|
Describe("Kubectl logs", func() {
|
||||||
It("should find a string in pod logs", func() {
|
var rcPath string
|
||||||
|
var nsFlag string
|
||||||
|
containerName := "redis-master"
|
||||||
|
BeforeEach(func() {
|
||||||
mkpath := func(file string) string {
|
mkpath := func(file string) string {
|
||||||
return filepath.Join(testContext.RepoRoot, "examples/guestbook-go", file)
|
return filepath.Join(testContext.RepoRoot, "examples/guestbook-go", file)
|
||||||
}
|
}
|
||||||
controllerJson := mkpath("redis-master-controller.json")
|
rcPath = mkpath("redis-master-controller.json")
|
||||||
nsFlag := fmt.Sprintf("--namespace=%v", ns)
|
By("creating an rc")
|
||||||
By("creating Redis RC")
|
nsFlag = fmt.Sprintf("--namespace=%v", ns)
|
||||||
runKubectl("create", "-f", controllerJson, nsFlag)
|
runKubectl("create", "-f", rcPath, nsFlag)
|
||||||
By("checking logs")
|
})
|
||||||
|
AfterEach(func() {
|
||||||
|
cleanup(rcPath, ns, simplePodSelector)
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should be able to retrieve and filter logs", func() {
|
||||||
forEachPod(c, ns, "app", "redis", func(pod api.Pod) {
|
forEachPod(c, ns, "app", "redis", func(pod api.Pod) {
|
||||||
_, err := lookForStringInLog(ns, pod.Name, "redis-master", "The server is now ready to accept connections", podStartTimeout)
|
By("checking for a matching strings")
|
||||||
|
_, err := lookForStringInLog(ns, pod.Name, containerName, "The server is now ready to accept connections", podStartTimeout)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("limiting log lines")
|
||||||
|
out := runKubectl("log", pod.Name, containerName, nsFlag, "--tail=1")
|
||||||
|
Expect(len(out)).NotTo(BeZero())
|
||||||
|
Expect(len(strings.Split(out, "\n"))).To(Equal(1))
|
||||||
|
|
||||||
|
By("limiting log bytes")
|
||||||
|
out = runKubectl("log", pod.Name, containerName, nsFlag, "--limit-bytes=1")
|
||||||
|
Expect(len(strings.Split(out, "\n"))).To(Equal(1))
|
||||||
|
Expect(len(out)).To(Equal(1))
|
||||||
|
|
||||||
|
By("exposing timestamps")
|
||||||
|
out = runKubectl("log", pod.Name, containerName, nsFlag, "--tail=1", "--timestamps")
|
||||||
|
lines := strings.Split(out, "\n")
|
||||||
|
Expect(len(lines)).To(Equal(1))
|
||||||
|
words := strings.Split(lines[0], " ")
|
||||||
|
Expect(len(words)).To(BeNumerically(">", 1))
|
||||||
|
if _, err := time.Parse(time.RFC3339Nano, words[0]); err != nil {
|
||||||
|
if _, err := time.Parse(time.RFC3339, words[0]); err != nil {
|
||||||
|
Failf("expected %q to be RFC3339 or RFC3339Nano", words[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
By("restricting to a time range")
|
||||||
|
time.Sleep(1500 * time.Millisecond) // ensure that startup logs on the node are seen as older than 1s
|
||||||
|
out = runKubectl("log", pod.Name, containerName, nsFlag, "--since=1s")
|
||||||
|
recent := len(strings.Split(out, "\n"))
|
||||||
|
out = runKubectl("log", pod.Name, containerName, nsFlag, "--since=24h")
|
||||||
|
older := len(strings.Split(out, "\n"))
|
||||||
|
Expect(recent).To(BeNumerically("<", older))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user