From 11fc0e55401a21a88c90a2fa646f2624541711fe Mon Sep 17 00:00:00 2001 From: Aditya Rajan Date: Fri, 3 Sep 2021 11:54:56 +0530 Subject: kube: Add support for podman pod logs Following PR adds support for `kubectl` like `pod logs` to podman. Usage `podman pod logs --- cmd/podman/pods/logs.go | 140 ++++++++++++++++++++++++++++++ docs/source/markdown/podman-pod-logs.1.md | 88 +++++++++++++++++++ docs/source/markdown/podman-pod.1.md | 5 +- docs/source/pod.rst | 2 + pkg/domain/entities/engine_container.go | 1 + pkg/domain/entities/pods.go | 27 ++++++ pkg/domain/infra/abi/pods.go | 40 +++++++++ pkg/domain/infra/tunnel/pods.go | 10 +++ test/e2e/play_kube_test.go | 34 ++++++++ 9 files changed, 345 insertions(+), 2 deletions(-) create mode 100644 cmd/podman/pods/logs.go create mode 100644 docs/source/markdown/podman-pod-logs.1.md diff --git a/cmd/podman/pods/logs.go b/cmd/podman/pods/logs.go new file mode 100644 index 000000000..fe5205669 --- /dev/null +++ b/cmd/podman/pods/logs.go @@ -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) +} diff --git a/docs/source/markdown/podman-pod-logs.1.md b/docs/source/markdown/podman-pod-logs.1.md new file mode 100644 index 000000000..8378f2eea --- /dev/null +++ b/docs/source/markdown/podman-pod-logs.1.md @@ -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) diff --git a/docs/source/markdown/podman-pod.1.md b/docs/source/markdown/podman-pod.1.md index e5a8207e9..9de2442bd 100644 --- a/docs/source/markdown/podman-pod.1.md +++ b/docs/source/markdown/podman-pod.1.md @@ -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. | | 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. | +| 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. | -| 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. | | 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. | | 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. | diff --git a/docs/source/pod.rst b/docs/source/pod.rst index 2df377762..d9ad07d83 100644 --- a/docs/source/pod.rst +++ b/docs/source/pod.rst @@ -9,6 +9,8 @@ Pod :doc:`kill ` Send the specified signal or SIGKILL to containers in pod +:doc:`logs ` Displays logs for pod with one or more containers + :doc:`pause ` Pause one or more pods :doc:`prune ` Remove all stopped pods and their containers diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index bd011d309..93fa7a20f 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -72,6 +72,7 @@ type ContainerEngine interface { PodExists(ctx context.Context, nameOrID string) (*BoolReport, error) PodInspect(ctx context.Context, options PodInspectOptions) (*PodInspectReport, 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) PodPrune(ctx context.Context, options PodPruneOptions) ([]*PodPruneReport, error) PodPs(ctx context.Context, options PodPSOptions) ([]*ListPodsReport, error) diff --git a/pkg/domain/entities/pods.go b/pkg/domain/entities/pods.go index 10bd7e5ce..d9dd0c532 100644 --- a/pkg/domain/entities/pods.go +++ b/pkg/domain/entities/pods.go @@ -133,6 +133,14 @@ type PodCreateOptions struct { 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 { Annotation []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") } } + +// 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 +} diff --git a/pkg/domain/infra/abi/pods.go b/pkg/domain/infra/abi/pods.go index 98233f60d..6b432c214 100644 --- a/pkg/domain/infra/abi/pods.go +++ b/pkg/domain/infra/abi/pods.go @@ -83,6 +83,46 @@ func (ic *ContainerEngine) PodKill(ctx context.Context, namesOrIds []string, opt 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 ` 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) { reports := []*entities.PodPauseReport{} pods, err := getPodsByContext(options.All, options.Latest, namesOrIds, ic.Libpod) diff --git a/pkg/domain/infra/tunnel/pods.go b/pkg/domain/infra/tunnel/pods.go index 480adb88a..8139216b3 100644 --- a/pkg/domain/infra/tunnel/pods.go +++ b/pkg/domain/infra/tunnel/pods.go @@ -42,6 +42,16 @@ func (ic *ContainerEngine) PodKill(ctx context.Context, namesOrIds []string, opt 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) { foundPods, err := getPodsByContext(ic.ClientCtx, options.All, namesOrIds) if err != nil { diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index ab496f0eb..5d875effd 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -1289,6 +1289,40 @@ var _ = Describe("Podman play kube", func() { 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() { // podName, set, expect testSli := [][]string{ -- cgit v1.2.3-54-g00ecf