diff --git a/cmd/flux/logs.go b/cmd/flux/logs.go index 6db6ef92..355cdfd0 100644 --- a/cmd/flux/logs.go +++ b/cmd/flux/logs.go @@ -26,12 +26,14 @@ import ( "os" "strings" "sync" + "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/kubectl/pkg/util" "github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/internal/utils" @@ -42,16 +44,19 @@ var logsCmd = &cobra.Command{ Short: "Display formatted logs for Flux components", Long: "The logs command displays formatted logs from various Flux components.", Example: ` # Print the reconciliation logs of all Flux custom resources in your cluster - flux logs --all-namespaces + flux logs --all-namespaces + + # Print all logs of all Flux custom resources newer than 2 minutes + flux logs --all-namespaces --since=2m - # Stream logs for a particular log level - flux logs --follow --level=error --all-namespaces + # Stream logs for a particular log level + flux logs --follow --level=error --all-namespaces - # Filter logs by kind, name and namespace - flux logs --kind=Kustomization --name=podinfo --namespace=default + # Filter logs by kind, name and namespace + flux logs --kind=Kustomization --name=podinfo --namespace=default - # Print logs when Flux is installed in a different namespace than flux-system - flux logs --flux-namespace=my-namespace + # Print logs when Flux is installed in a different namespace than flux-system + flux logs --flux-namespace=my-namespace `, RunE: logsCmdRun, } @@ -64,6 +69,8 @@ type logsFlags struct { name string fluxNamespace string allNamespaces bool + sinceTime string + sinceSeconds time.Duration } var logsArgs = &logsFlags{ @@ -78,6 +85,8 @@ func init() { logsCmd.Flags().Int64VarP(&logsArgs.tail, "tail", "", logsArgs.tail, "lines of recent log file to display") logsCmd.Flags().StringVarP(&logsArgs.fluxNamespace, "flux-namespace", "", rootArgs.defaults.Namespace, "the namespace where the Flux components are running") logsCmd.Flags().BoolVarP(&logsArgs.allNamespaces, "all-namespaces", "A", false, "displays logs for objects across all namespaces") + logsCmd.Flags().DurationVar(&logsArgs.sinceSeconds, "since", logsArgs.sinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") + logsCmd.Flags().StringVar(&logsArgs.sinceTime, "since-time", logsArgs.sinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.") rootCmd.AddCommand(logsCmd) } @@ -87,7 +96,6 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - var pods []corev1.Pod cfg, err := utils.KubeConfig(rootArgs.kubeconfig, rootArgs.kubecontext) if err != nil { return err @@ -102,7 +110,7 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("no argument required") } - pods, err = getPods(ctx, clientset, fluxSelector) + pods, err := getPods(ctx, clientset, fluxSelector) if err != nil { return err } @@ -115,6 +123,24 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { logOpts.TailLines = &logsArgs.tail } + if len(logsArgs.sinceTime) > 0 && logsArgs.sinceSeconds != 0 { + return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified") + } + + if len(logsArgs.sinceTime) > 0 { + t, err := util.ParseRFC3339(logsArgs.sinceTime, metav1.Now) + if err != nil { + return fmt.Errorf("%s is not a valid (RFC3339) time", logsArgs.sinceTime) + } + logOpts.SinceTime = &t + } + + if logsArgs.sinceSeconds != 0 { + // round up to the nearest second + sec := int64(logsArgs.sinceSeconds.Round(time.Second).Seconds()) + logOpts.SinceSeconds = &sec + } + var requests []rest.ResponseWrapper for _, pod := range pods { req := clientset.CoreV1().Pods(logsArgs.fluxNamespace).GetLogs(pod.Name, logOpts) diff --git a/cmd/flux/logs_test.go b/cmd/flux/logs_test.go new file mode 100644 index 00000000..d6821410 --- /dev/null +++ b/cmd/flux/logs_test.go @@ -0,0 +1,63 @@ +// +build unit + +package main + +import ( + "testing" +) + +func TestLogsNoArgs(t *testing.T) { + cmd := cmdTestCase{ + args: "logs", + assert: assertSuccess(), + } + cmd.runTestCmd(t) +} + +func TestLogsAllNamespaces(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --all-namespaces", + assert: assertSuccess(), + } + cmd.runTestCmd(t) +} + +func TestLogsSince(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --since=2m", + assert: assertSuccess(), + } + cmd.runTestCmd(t) +} + +func TestLogsSinceInvalid(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --since=XXX", + assert: assertError(`invalid argument "XXX" for "--since" flag: time: invalid duration "XXX"`), + } + cmd.runTestCmd(t) +} + +func TestLogsSinceTime(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --since-time=2021-08-06T14:26:25.546Z", + assert: assertSuccess(), + } + cmd.runTestCmd(t) +} + +func TestLogsSinceTimeInvalid(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --since-time=XXX", + assert: assertError("XXX is not a valid (RFC3339) time"), + } + cmd.runTestCmd(t) +} + +func TestLogsSinceOnlyOneAllowed(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --since=2m --since-time=2021-08-06T14:26:25.546Z", + assert: assertError("at most one of `sinceTime` or `sinceSeconds` may be specified"), + } + cmd.runTestCmd(t) +}