kube: Add support for podman pod logs

Following PR adds support for `kubectl` like `pod logs` to podman.
Usage `podman pod logs <podIDorName` gives a stream of logs for all
the containers within the pod with **containername** as a field.

Just like **`kubectl`** also supports `podman pod logs -c ctrIDorName podIDorName`
to limit the log stream to any of the specificied container which belongs to pod.

Signed-off-by: Aditya Rajan <arajan@redhat.com>
This commit is contained in:
Aditya Rajan 2021-09-03 11:54:56 +05:30
parent 858d3e47c2
commit 11fc0e5540
9 changed files with 345 additions and 2 deletions

140
cmd/podman/pods/logs.go Normal file
View file

@ -0,0 +1,140 @@
package pods
import (
"os"
"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/cmd/podman/validate"
"github.com/containers/podman/v3/libpod/define"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/containers/podman/v3/pkg/util"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// logsOptionsWrapper wraps entities.LogsOptions and prevents leaking
// CLI-only fields into the API types.
type logsOptionsWrapper struct {
entities.PodLogsOptions
SinceRaw string
UntilRaw string
}
var (
logsPodOptions logsOptionsWrapper
logsPodDescription = `Displays logs for pod with one or more containers.`
logsPodCommand = &cobra.Command{
Use: "logs [options] POD",
Short: "Fetch logs for pod with one or more containers",
Long: logsPodDescription,
// We dont want users to invoke latest and pod togather
Args: func(cmd *cobra.Command, args []string) error {
switch {
case registry.IsRemote() && logsPodOptions.Latest:
return errors.New(cmd.Name() + " does not support 'latest' when run remotely")
case len(args) > 1:
return errors.New("requires exactly 1 arg")
case logsPodOptions.Latest && len(args) > 0:
return errors.New("--latest and pods cannot be used together")
case !logsPodOptions.Latest && len(args) < 1:
return errors.New("specify at least one pod name or ID to log")
}
return nil
},
RunE: logs,
ValidArgsFunction: common.AutocompletePods,
Example: `podman pod logs podID
podman pod logs -c ctrname podName
podman pod logs --tail 2 mywebserver
podman pod logs --follow=true --since 10m podID
podman pod logs mywebserver`,
}
containerLogsCommand = &cobra.Command{
Use: logsPodCommand.Use,
Short: logsPodCommand.Short,
Long: logsPodCommand.Long,
Args: logsPodCommand.Args,
RunE: logsPodCommand.RunE,
ValidArgsFunction: logsPodCommand.ValidArgsFunction,
Example: `podman pod logs podId
podman pod logs -c ctrname podName
podman pod logs --tail 2 mywebserver
podman pod logs --follow=true --since 10m podID`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: logsPodCommand,
})
logsFlags(logsPodCommand)
validate.AddLatestFlag(logsPodCommand, &logsPodOptions.Latest)
// container logs
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: containerLogsCommand,
Parent: podCmd,
})
logsFlags(containerLogsCommand)
validate.AddLatestFlag(containerLogsCommand, &logsPodOptions.Latest)
}
func logsFlags(cmd *cobra.Command) {
flags := cmd.Flags()
flags.BoolVar(&logsPodOptions.Details, "details", false, "Show extra details provided to the logs")
flags.BoolVarP(&logsPodOptions.Follow, "follow", "f", false, "Follow log output.")
containerNameFlag := "container"
flags.StringVarP(&logsPodOptions.ContainerName, containerNameFlag, "c", "", "Filter logs by container name or id which belongs to pod")
_ = cmd.RegisterFlagCompletionFunc(containerNameFlag, common.AutocompleteContainers)
sinceFlagName := "since"
flags.StringVar(&logsPodOptions.SinceRaw, sinceFlagName, "", "Show logs since TIMESTAMP")
_ = cmd.RegisterFlagCompletionFunc(sinceFlagName, completion.AutocompleteNone)
untilFlagName := "until"
flags.StringVar(&logsPodOptions.UntilRaw, untilFlagName, "", "Show logs until TIMESTAMP")
_ = cmd.RegisterFlagCompletionFunc(untilFlagName, completion.AutocompleteNone)
tailFlagName := "tail"
flags.Int64Var(&logsPodOptions.Tail, tailFlagName, -1, "Output the specified number of LINES at the end of the logs.")
_ = cmd.RegisterFlagCompletionFunc(tailFlagName, completion.AutocompleteNone)
flags.BoolVarP(&logsPodOptions.Timestamps, "timestamps", "t", false, "Output the timestamps in the log")
flags.SetInterspersed(false)
_ = flags.MarkHidden("details")
}
func logs(_ *cobra.Command, args []string) error {
if logsPodOptions.SinceRaw != "" {
// parse time, error out if something is wrong
since, err := util.ParseInputTime(logsPodOptions.SinceRaw, true)
if err != nil {
return errors.Wrapf(err, "error parsing --since %q", logsPodOptions.SinceRaw)
}
logsPodOptions.Since = since
}
if logsPodOptions.UntilRaw != "" {
// parse time, error out if something is wrong
until, err := util.ParseInputTime(logsPodOptions.UntilRaw, false)
if err != nil {
return errors.Wrapf(err, "error parsing --until %q", logsPodOptions.UntilRaw)
}
logsPodOptions.Until = until
}
// Remote can only process one container at a time
if registry.IsRemote() && logsPodOptions.ContainerName == "" {
return errors.Wrapf(define.ErrInvalidArg, "-c or --container cannot be empty")
}
logsPodOptions.StdoutWriter = os.Stdout
logsPodOptions.StderrWriter = os.Stderr
return registry.ContainerEngine().PodLogs(registry.GetContext(), args[0], logsPodOptions.PodLogsOptions)
}

View file

@ -0,0 +1,88 @@
% podman-pod-logs(1)
## NAME
podman\-pod\-logs - Displays logs for pod with one or more containers
## SYNOPSIS
**podman pod logs** [*options*] *pod*
## DESCRIPTION
The podman pod logs command batch-retrieves whatever logs are present with all the containers of a pod. Pod logs can be filtered by container name or id using flag **-c** or **--container** if needed.
Note: Long running command of `podman pod log` with a `-f` or `--follow` needs to be reinvoked if new container is added to the pod dynamically otherwise logs of newly added containers would not be visible in log stream.
## OPTIONS
#### **--container**, **-c**
By default `podman pod logs` retrives logs for all the containers available within the pod differentiate by field `container`. However there are use-cases where user would want to limit the log stream only to a particular container of a pod for such cases `-c` can be used like `podman pod logs -c ctrNameorID podname`.
#### **--follow**, **-f**
Follow log output. Default is false.
Note: If you are following a pod which is removed `podman pod rm`, then there is a
chance the the log file will be removed before `podman pod logs` reads the final content.
#### **--latest**, **-l**
Instead of providing the pod name or id, get logs of the last created pod. (This option is not available with the remote Podman client)
#### **--since**=*TIMESTAMP*
Show logs since TIMESTAMP. The --since option can be Unix timestamps, date formatted timestamps, or Go duration
strings (e.g. 10m, 1h30m) computed relative to the client machine's time. Supported formats for date formatted
time stamps include RFC3339Nano, RFC3339, 2006-01-02T15:04:05, 2006-01-02T15:04:05.999999999, 2006-01-02Z07:00,
and 2006-01-02.
#### **--until**=*TIMESTAMP*
Show logs until TIMESTAMP. The --until option can be Unix timestamps, date formatted timestamps, or Go duration
strings (e.g. 10m, 1h30m) computed relative to the client machine's time. Supported formats for date formatted
time stamps include RFC3339Nano, RFC3339, 2006-01-02T15:04:05, 2006-01-02T15:04:05.999999999, 2006-01-02Z07:00,
and 2006-01-02.
#### **--tail**=*LINES*
Output the specified number of LINES at the end of the logs. LINES must be an integer. Defaults to -1,
which prints all lines
#### **--timestamps**, **-t**
Show timestamps in the log outputs. The default is false
## EXAMPLE
To view a pod's logs:
```
podman pod logs -t podIdorName
```
To view logs of a specific container on the pod
```
podman pod logs -c ctrIdOrName podIdOrName
```
To view all pod logs:
```
podman pod logs -t --since 0 myserver-pod-1
```
To view a pod's logs since a certain time:
```
podman pod logs -t --since 2017-08-07T10:10:09.055837383-04:00 myserver-pod-1
```
To view a pod's logs generated in the last 10 minutes:
```
podman pod logs --since 10m myserver-pod-1
```
To view a pod's logs until 30 minutes ago:
```
podman pod logs --until 30m myserver-pod-1
```
## SEE ALSO
podman(1), podman-pod-start(1), podman-pod-rm(1), podman-logs(1)

View file

@ -17,11 +17,12 @@ podman pod is a set of subcommands that manage pods, or groups of containers.
| exists | [podman-pod-exists(1)](podman-pod-exists.1.md) | Check if a pod exists in local storage. | | exists | [podman-pod-exists(1)](podman-pod-exists.1.md) | Check if a pod exists in local storage. |
| inspect | [podman-pod-inspect(1)](podman-pod-inspect.1.md) | Displays information describing a pod. | | inspect | [podman-pod-inspect(1)](podman-pod-inspect.1.md) | Displays information describing a pod. |
| kill | [podman-pod-kill(1)](podman-pod-kill.1.md) | Kill the main process of each container in one or more pods. | | kill | [podman-pod-kill(1)](podman-pod-kill.1.md) | Kill the main process of each container in one or more pods. |
| logs | [podman-pod-logs(1)](podman-pod-logs.1.md) | Displays logs for pod with one or more containers. |
| pause | [podman-pod-pause(1)](podman-pod-pause.1.md) | Pause one or more pods. | | pause | [podman-pod-pause(1)](podman-pod-pause.1.md) | Pause one or more pods. |
| prune | [podman-pod-prune(1)](podman-pod-prune.1.md) | Remove all stopped pods and their containers. | | prune | [podman-pod-prune(1)](podman-pod-prune.1.md) | Remove all stopped pods and their containers. |
| ps | [podman-pod-ps(1)](podman-pod-ps.1.md) | Prints out information about pods. | | ps | [podman-pod-ps(1)](podman-pod-ps.1.md) | Prints out information about pods. |
| restart | [podman-pod-restart(1)](podman-pod-restart.1.md) | Restart one or more pods. | | restart | [podman-pod-restart(1)](podman-pod-restart.1.md) | Restart one or more pods. |
| rm | [podman-pod-rm(1)](podman-pod-rm.1.md) | Remove one or more stopped pods and containers. | | rm | [podman-pod-rm(1)](podman-pod-rm.1.md) | Remove one or more stopped pods and containers. |
| start | [podman-pod-start(1)](podman-pod-start.1.md) | Start one or more pods. | | start | [podman-pod-start(1)](podman-pod-start.1.md) | Start one or more pods. |
| stats | [podman-pod-stats(1)](podman-pod-stats.1.md) | Display a live stream of resource usage stats for containers in one or more pods. | | stats | [podman-pod-stats(1)](podman-pod-stats.1.md) | Display a live stream of resource usage stats for containers in one or more pods. |
| stop | [podman-pod-stop(1)](podman-pod-stop.1.md) | Stop one or more pods. | | stop | [podman-pod-stop(1)](podman-pod-stop.1.md) | Stop one or more pods. |

View file

@ -9,6 +9,8 @@ Pod
:doc:`kill <markdown/podman-pod-kill.1>` Send the specified signal or SIGKILL to containers in pod :doc:`kill <markdown/podman-pod-kill.1>` Send the specified signal or SIGKILL to containers in pod
:doc:`logs <markdown/podman-pod-logs.1>` Displays logs for pod with one or more containers
:doc:`pause <markdown/podman-pause.1>` Pause one or more pods :doc:`pause <markdown/podman-pause.1>` Pause one or more pods
:doc:`prune <markdown/podman-pod-prune.1>` Remove all stopped pods and their containers :doc:`prune <markdown/podman-pod-prune.1>` Remove all stopped pods and their containers

View file

@ -72,6 +72,7 @@ type ContainerEngine interface {
PodExists(ctx context.Context, nameOrID string) (*BoolReport, error) PodExists(ctx context.Context, nameOrID string) (*BoolReport, error)
PodInspect(ctx context.Context, options PodInspectOptions) (*PodInspectReport, error) PodInspect(ctx context.Context, options PodInspectOptions) (*PodInspectReport, error)
PodKill(ctx context.Context, namesOrIds []string, options PodKillOptions) ([]*PodKillReport, error) PodKill(ctx context.Context, namesOrIds []string, options PodKillOptions) ([]*PodKillReport, error)
PodLogs(ctx context.Context, pod string, options PodLogsOptions) error
PodPause(ctx context.Context, namesOrIds []string, options PodPauseOptions) ([]*PodPauseReport, error) PodPause(ctx context.Context, namesOrIds []string, options PodPauseOptions) ([]*PodPauseReport, error)
PodPrune(ctx context.Context, options PodPruneOptions) ([]*PodPruneReport, error) PodPrune(ctx context.Context, options PodPruneOptions) ([]*PodPruneReport, error)
PodPs(ctx context.Context, options PodPSOptions) ([]*ListPodsReport, error) PodPs(ctx context.Context, options PodPSOptions) ([]*ListPodsReport, error)

View file

@ -133,6 +133,14 @@ type PodCreateOptions struct {
Userns specgen.Namespace Userns specgen.Namespace
} }
// PodLogsOptions describes the options to extract pod logs.
type PodLogsOptions struct {
// Other fields are exactly same as ContainerLogOpts
ContainerLogsOptions
// If specified will only fetch the logs of specified container
ContainerName string
}
type ContainerCreateOptions struct { type ContainerCreateOptions struct {
Annotation []string Annotation []string
Attach []string Attach []string
@ -426,3 +434,22 @@ func ValidatePodStatsOptions(args []string, options *PodStatsOptions) error {
return errors.New("--all, --latest and arguments cannot be used together") return errors.New("--all, --latest and arguments cannot be used together")
} }
} }
// Converts PodLogOptions to ContainerLogOptions
func PodLogsOptionsToContainerLogsOptions(options PodLogsOptions) ContainerLogsOptions {
// PodLogsOptions are similar but contains few extra fields like ctrName
// So cast other values as is so we can re-use the code
containerLogsOpts := ContainerLogsOptions{
Details: options.Details,
Latest: options.Latest,
Follow: options.Follow,
Names: options.Names,
Since: options.Since,
Until: options.Until,
Tail: options.Tail,
Timestamps: options.Timestamps,
StdoutWriter: options.StdoutWriter,
StderrWriter: options.StderrWriter,
}
return containerLogsOpts
}

View file

@ -83,6 +83,46 @@ func (ic *ContainerEngine) PodKill(ctx context.Context, namesOrIds []string, opt
return reports, nil return reports, nil
} }
func (ic *ContainerEngine) PodLogs(ctx context.Context, nameOrID string, options entities.PodLogsOptions) error {
// Implementation accepts slice
podName := []string{nameOrID}
pod, err := getPodsByContext(false, options.Latest, podName, ic.Libpod)
if err != nil {
return err
}
// Get pod containers
podCtrs, err := pod[0].AllContainers()
if err != nil {
return err
}
ctrNames := []string{}
// Check if `kubectl pod logs -c ctrname <podname>` alike command is used
if options.ContainerName != "" {
ctrFound := false
for _, ctr := range podCtrs {
if ctr.ID() == options.ContainerName || ctr.Name() == options.ContainerName {
ctrNames = append(ctrNames, options.ContainerName)
ctrFound = true
}
}
if !ctrFound {
return errors.Wrapf(define.ErrNoSuchCtr, "container %s is not in pod %s", options.ContainerName, nameOrID)
}
} else {
// No container name specified select all containers
for _, ctr := range podCtrs {
ctrNames = append(ctrNames, ctr.Name())
}
}
// PodLogsOptions are similar but contains few extra fields like ctrName
// So cast other values as is so we can re-use the code
containerLogsOpts := entities.PodLogsOptionsToContainerLogsOptions(options)
return ic.ContainerLogs(ctx, ctrNames, containerLogsOpts)
}
func (ic *ContainerEngine) PodPause(ctx context.Context, namesOrIds []string, options entities.PodPauseOptions) ([]*entities.PodPauseReport, error) { func (ic *ContainerEngine) PodPause(ctx context.Context, namesOrIds []string, options entities.PodPauseOptions) ([]*entities.PodPauseReport, error) {
reports := []*entities.PodPauseReport{} reports := []*entities.PodPauseReport{}
pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod)

View file

@ -42,6 +42,16 @@ func (ic *ContainerEngine) PodKill(ctx context.Context, namesOrIds []string, opt
return reports, nil return reports, nil
} }
func (ic *ContainerEngine) PodLogs(_ context.Context, nameOrIDs string, options entities.PodLogsOptions) error {
// PodLogsOptions are similar but contains few extra fields like ctrName
// So cast other values as is so we can re-use the code
containerLogsOpts := entities.PodLogsOptionsToContainerLogsOptions(options)
// interface only accepts slice, keep everything consistent
name := []string{options.ContainerName}
return ic.ContainerLogs(nil, name, containerLogsOpts)
}
func (ic *ContainerEngine) PodPause(ctx context.Context, namesOrIds []string, options entities.PodPauseOptions) ([]*entities.PodPauseReport, error) { func (ic *ContainerEngine) PodPause(ctx context.Context, namesOrIds []string, options entities.PodPauseOptions) ([]*entities.PodPauseReport, error) {
foundPods, err := getPodsByContext(ic.ClientCtx, options.All, namesOrIds) foundPods, err := getPodsByContext(ic.ClientCtx, options.All, namesOrIds)
if err != nil { if err != nil {

View file

@ -1289,6 +1289,40 @@ var _ = Describe("Podman play kube", func() {
Expect(logs.OutputToString()).To(ContainSubstring("hello world")) Expect(logs.OutputToString()).To(ContainSubstring("hello world"))
}) })
It("podman pod logs test", func() {
SkipIfRemote("podman-remote pod logs -c is mandatory for remote machine")
p := getPod(withCtr(getCtr(withCmd([]string{"echo", "hello"}), withArg([]string{"world"}))))
err := generateKubeYaml("pod", p, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube).Should(Exit(0))
logs := podmanTest.Podman([]string{"pod", "logs", p.Name})
logs.WaitWithDefaultTimeout()
Expect(logs).Should(Exit(0))
Expect(logs.OutputToString()).To(ContainSubstring("hello world"))
})
It("podman-remote pod logs test", func() {
// -c or --container is required in podman-remote due to api limitation.
p := getPod(withCtr(getCtr(withCmd([]string{"echo", "hello"}), withArg([]string{"world"}))))
err := generateKubeYaml("pod", p, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube).Should(Exit(0))
logs := podmanTest.Podman([]string{"pod", "logs", "-c", getCtrNameInPod(p), p.Name})
logs.WaitWithDefaultTimeout()
Expect(logs).Should(Exit(0))
Expect(logs.OutputToString()).To(ContainSubstring("hello world"))
})
It("podman play kube test restartPolicy", func() { It("podman play kube test restartPolicy", func() {
// podName, set, expect // podName, set, expect
testSli := [][]string{ testSli := [][]string{