mirror of
https://github.com/fluxcd/flux2.git
synced 2026-02-20 06:31:47 +00:00
Merge pull request #3945 from fluxcd/lenient-logs-cmd
Make `flux logs` more lenient
This commit is contained in:
commit
a3f2b1d829
4 changed files with 138 additions and 73 deletions
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -76,7 +77,7 @@ type logsFlags struct {
|
||||||
sinceSeconds time.Duration
|
sinceSeconds time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var logsArgs = &logsFlags{
|
var logsArgs = logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,7 +116,7 @@ func logsCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return fmt.Errorf("no argument required")
|
return fmt.Errorf("no argument required")
|
||||||
}
|
}
|
||||||
|
|
||||||
pods, err := getPods(ctx, clientset, fluxSelector)
|
pods, err := getPods(ctx, clientset, logsArgs.fluxNamespace, fluxSelector)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -163,13 +164,16 @@ func logsCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
return podLogs(ctx, requests)
|
return podLogs(ctx, requests)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPods(ctx context.Context, c *kubernetes.Clientset, label string) ([]corev1.Pod, error) {
|
// getPods searches for all Deployments in the given namespace that match the given label and returns a list of Pods
|
||||||
|
// from these Deployments. For each Deployment a single Pod is chosen (based on various factors such as the running
|
||||||
|
// state). If no Pod is found, an error is returned.
|
||||||
|
func getPods(ctx context.Context, c *kubernetes.Clientset, ns string, label string) ([]corev1.Pod, error) {
|
||||||
var ret []corev1.Pod
|
var ret []corev1.Pod
|
||||||
|
|
||||||
opts := metav1.ListOptions{
|
opts := metav1.ListOptions{
|
||||||
LabelSelector: label,
|
LabelSelector: label,
|
||||||
}
|
}
|
||||||
deployList, err := c.AppsV1().Deployments(logsArgs.fluxNamespace).List(ctx, opts)
|
deployList, err := c.AppsV1().Deployments(ns).List(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
@ -179,7 +183,7 @@ func getPods(ctx context.Context, c *kubernetes.Clientset, label string) ([]core
|
||||||
opts := metav1.ListOptions{
|
opts := metav1.ListOptions{
|
||||||
LabelSelector: createLabelStringFromMap(label),
|
LabelSelector: createLabelStringFromMap(label),
|
||||||
}
|
}
|
||||||
podList, err := c.CoreV1().Pods(logsArgs.fluxNamespace).List(ctx, opts)
|
podList, err := c.CoreV1().Pods(ns).List(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ret, err
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
@ -196,11 +200,16 @@ func getPods(ctx context.Context, c *kubernetes.Clientset, label string) ([]core
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
return nil, fmt.Errorf("no Flux pods found in namespace %q", ns)
|
||||||
|
}
|
||||||
|
|
||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error {
|
func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error {
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe()
|
||||||
|
errReader, errWriter := io.Pipe()
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(requests))
|
wg.Add(len(requests))
|
||||||
|
|
||||||
|
|
@ -208,7 +217,7 @@ func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error
|
||||||
go func(req rest.ResponseWrapper) {
|
go func(req rest.ResponseWrapper) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := logRequest(ctx, req, writer); err != nil {
|
if err := logRequest(ctx, req, writer); err != nil {
|
||||||
writer.CloseWithError(err)
|
fmt.Fprintf(errWriter, "failed getting logs: %s\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}(request)
|
}(request)
|
||||||
|
|
@ -217,20 +226,40 @@ func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error
|
||||||
go func() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
writer.Close()
|
writer.Close()
|
||||||
|
errWriter.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err := io.Copy(os.Stdout, reader)
|
stdoutErrCh := asyncCopy(os.Stdout, reader)
|
||||||
return err
|
stderrErrCh := asyncCopy(os.Stderr, errReader)
|
||||||
|
|
||||||
|
return errors.Join(<-stdoutErrCh, <-stderrErrCh)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asyncCopy copies all data from from dst to src asynchronously and returns a channel for reading an error value.
|
||||||
|
// This is basically an asynchronous wrapper around `io.Copy`. The returned channel is unbuffered and always is sent
|
||||||
|
// a value (either nil or the error from `io.Copy`) as soon as `io.Copy` returns.
|
||||||
|
// This function lets you copy from multiple sources into multiple destinations in parallel.
|
||||||
|
func asyncCopy(dst io.Writer, src io.Reader) <-chan error {
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func(errCh chan error) {
|
||||||
|
_, err := io.Copy(dst, src)
|
||||||
|
errCh <- err
|
||||||
|
}(errCh)
|
||||||
|
|
||||||
|
return errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func podLogs(ctx context.Context, requests []rest.ResponseWrapper) error {
|
func podLogs(ctx context.Context, requests []rest.ResponseWrapper) error {
|
||||||
|
var retErr error
|
||||||
for _, req := range requests {
|
for _, req := range requests {
|
||||||
if err := logRequest(ctx, req, os.Stdout); err != nil {
|
if err := logRequest(ctx, req, os.Stdout); err != nil {
|
||||||
return err
|
fmt.Fprintf(os.Stderr, "failed getting logs: %s\n", err)
|
||||||
|
retErr = fmt.Errorf("failed to collect logs from all Flux pods")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return retErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func createLabelStringFromMap(m map[string]string) string {
|
func createLabelStringFromMap(m map[string]string) string {
|
||||||
|
|
|
||||||
88
cmd/flux/logs_e2e_test.go
Normal file
88
cmd/flux/logs_e2e_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
//go:build e2e
|
||||||
|
// +build e2e
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Flux authors
|
||||||
|
|
||||||
|
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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogsNoArgs(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "logs",
|
||||||
|
assert: assertSuccess(),
|
||||||
|
}
|
||||||
|
cmd.runTestCmd(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogsWrongNamespace(t *testing.T) {
|
||||||
|
cmd := cmdTestCase{
|
||||||
|
args: "logs --flux-namespace=default",
|
||||||
|
assert: assertError(`no Flux pods found in namespace "default"`),
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -30,73 +30,17 @@ import (
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLogRequest(t *testing.T) {
|
func TestLogRequest(t *testing.T) {
|
||||||
mapper := &testResponseMapper{}
|
mapper := &testResponseMapper{}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
namespace string
|
namespace string
|
||||||
flags *logsFlags
|
flags logsFlags
|
||||||
assertFile string
|
assertFile string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "all logs",
|
name: "all logs",
|
||||||
flags: &logsFlags{
|
flags: logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
allNamespaces: true,
|
allNamespaces: true,
|
||||||
},
|
},
|
||||||
|
|
@ -105,14 +49,14 @@ func TestLogRequest(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "filter by namespace",
|
name: "filter by namespace",
|
||||||
namespace: "default",
|
namespace: "default",
|
||||||
flags: &logsFlags{
|
flags: logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
},
|
},
|
||||||
assertFile: "testdata/logs/namespace.txt",
|
assertFile: "testdata/logs/namespace.txt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "filter by kind and namespace",
|
name: "filter by kind and namespace",
|
||||||
flags: &logsFlags{
|
flags: logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
kind: "Kustomization",
|
kind: "Kustomization",
|
||||||
},
|
},
|
||||||
|
|
@ -120,7 +64,7 @@ func TestLogRequest(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "filter by loglevel",
|
name: "filter by loglevel",
|
||||||
flags: &logsFlags{
|
flags: logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
logLevel: "error",
|
logLevel: "error",
|
||||||
allNamespaces: true,
|
allNamespaces: true,
|
||||||
|
|
@ -130,7 +74,7 @@ func TestLogRequest(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "filter by namespace, name, loglevel and kind",
|
name: "filter by namespace, name, loglevel and kind",
|
||||||
namespace: "flux-system",
|
namespace: "flux-system",
|
||||||
flags: &logsFlags{
|
flags: logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
logLevel: "error",
|
logLevel: "error",
|
||||||
kind: "Kustomization",
|
kind: "Kustomization",
|
||||||
|
|
@ -163,7 +107,7 @@ func TestLogRequest(t *testing.T) {
|
||||||
|
|
||||||
// reset flags to default
|
// reset flags to default
|
||||||
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
||||||
logsArgs = &logsFlags{
|
logsArgs = logsFlags{
|
||||||
tail: -1,
|
tail: -1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -392,6 +392,10 @@ func resetCmdArgs() {
|
||||||
alertProviderArgs = alertProviderFlags{}
|
alertProviderArgs = alertProviderFlags{}
|
||||||
bootstrapArgs = NewBootstrapFlags()
|
bootstrapArgs = NewBootstrapFlags()
|
||||||
bServerArgs = bServerFlags{}
|
bServerArgs = bServerFlags{}
|
||||||
|
logsArgs = logsFlags{
|
||||||
|
tail: -1,
|
||||||
|
fluxNamespace: rootArgs.defaults.Namespace,
|
||||||
|
}
|
||||||
buildKsArgs = buildKsFlags{}
|
buildKsArgs = buildKsFlags{}
|
||||||
checkArgs = checkFlags{}
|
checkArgs = checkFlags{}
|
||||||
createArgs = createFlags{}
|
createArgs = createFlags{}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue