mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-19 09:52:49 +00:00
Add new flags into alpha events
In order to promote kubectl alpha events to beta, it should at least support flags which is already supported by kubectl get events as well as new flags. This PR adds; --output: json|yaml support and does essential refactorings to integrate other printing options easier in the future. --no-headers: kubectl get events can hide headers when this flag is set for default printing. Adds this ability to hide headers also for kubectl alpha events. This flag has no effect when output is json or yaml for both commands. --types: This will be used to filter certain events to be printed and discard others(default behavior is same with --event=Normal,Warning).
This commit is contained in:
parent
ce433f87b4
commit
63b8684cd3
124
staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go
Normal file
124
staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go
Normal file
@ -0,0 +1,124 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes 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 events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
)
|
||||
|
||||
// EventPrinter stores required fields to be used for
|
||||
// default printing for events command.
|
||||
type EventPrinter struct {
|
||||
NoHeaders bool
|
||||
AllNamespaces bool
|
||||
|
||||
headersPrinted bool
|
||||
}
|
||||
|
||||
// PrintObj prints different type of event objects.
|
||||
func (ep *EventPrinter) PrintObj(obj runtime.Object, out io.Writer) error {
|
||||
if !ep.NoHeaders && !ep.headersPrinted {
|
||||
ep.printHeadings(out)
|
||||
ep.headersPrinted = true
|
||||
}
|
||||
|
||||
switch t := obj.(type) {
|
||||
case *corev1.EventList:
|
||||
for _, e := range t.Items {
|
||||
ep.printOneEvent(out, e)
|
||||
}
|
||||
case *corev1.Event:
|
||||
ep.printOneEvent(out, *t)
|
||||
default:
|
||||
return fmt.Errorf("unknown event type %t", t)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *EventPrinter) printHeadings(w io.Writer) {
|
||||
if ep.AllNamespaces {
|
||||
fmt.Fprintf(w, "NAMESPACE\t")
|
||||
}
|
||||
fmt.Fprintf(w, "LAST SEEN\tTYPE\tREASON\tOBJECT\tMESSAGE\n")
|
||||
}
|
||||
|
||||
func (ep *EventPrinter) printOneEvent(w io.Writer, e corev1.Event) {
|
||||
interval := getInterval(e)
|
||||
if ep.AllNamespaces {
|
||||
fmt.Fprintf(w, "%v\t", e.Namespace)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s/%s\t%v\n",
|
||||
interval,
|
||||
e.Type,
|
||||
e.Reason,
|
||||
e.InvolvedObject.Kind, e.InvolvedObject.Name,
|
||||
strings.TrimSpace(e.Message),
|
||||
)
|
||||
}
|
||||
|
||||
func getInterval(e corev1.Event) string {
|
||||
var interval string
|
||||
firstTimestampSince := translateMicroTimestampSince(e.EventTime)
|
||||
if e.EventTime.IsZero() {
|
||||
firstTimestampSince = translateTimestampSince(e.FirstTimestamp)
|
||||
}
|
||||
if e.Series != nil {
|
||||
interval = fmt.Sprintf("%s (x%d over %s)", translateMicroTimestampSince(e.Series.LastObservedTime), e.Series.Count, firstTimestampSince)
|
||||
} else if e.Count > 1 {
|
||||
interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, firstTimestampSince)
|
||||
} else {
|
||||
interval = firstTimestampSince
|
||||
}
|
||||
|
||||
return interval
|
||||
}
|
||||
|
||||
// translateMicroTimestampSince returns the elapsed time since timestamp in
|
||||
// human-readable approximation.
|
||||
func translateMicroTimestampSince(timestamp metav1.MicroTime) string {
|
||||
if timestamp.IsZero() {
|
||||
return "<unknown>"
|
||||
}
|
||||
|
||||
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||
}
|
||||
|
||||
// translateTimestampSince returns the elapsed time since timestamp in
|
||||
// human-readable approximation.
|
||||
func translateTimestampSince(timestamp metav1.Time) string {
|
||||
if timestamp.IsZero() {
|
||||
return "<unknown>"
|
||||
}
|
||||
|
||||
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||
}
|
||||
|
||||
func NewEventPrinter(noHeader, allNamespaces bool) *EventPrinter {
|
||||
return &EventPrinter{
|
||||
NoHeaders: noHeader,
|
||||
AllNamespaces: allNamespaces,
|
||||
}
|
||||
}
|
226
staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go
Normal file
226
staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go
Normal file
@ -0,0 +1,226 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes 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 events
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPrintObj(t *testing.T) {
|
||||
tests := []struct {
|
||||
printer EventPrinter
|
||||
obj runtime.Object
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
printer: EventPrinter{
|
||||
NoHeaders: false,
|
||||
AllNamespaces: false,
|
||||
},
|
||||
obj: &corev1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-000",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
expected: `LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
printer: EventPrinter{
|
||||
NoHeaders: false,
|
||||
AllNamespaces: true,
|
||||
},
|
||||
obj: &corev1.EventList{
|
||||
Items: []corev1.Event{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-000",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-001",
|
||||
Namespace: "bar",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar2",
|
||||
Namespace: "foo2",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-11 * time.Minute)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
bar 11m (x3 over 15m) Normal ScalingReplicaSet Deployment/bar2 Scaled up replica set bar-002 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
printer: EventPrinter{
|
||||
NoHeaders: true,
|
||||
AllNamespaces: false,
|
||||
},
|
||||
obj: &corev1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-000",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
expected: "12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1\n",
|
||||
},
|
||||
{
|
||||
printer: EventPrinter{
|
||||
NoHeaders: false,
|
||||
AllNamespaces: true,
|
||||
},
|
||||
obj: &corev1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-000",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
expected: `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
printer: EventPrinter{
|
||||
NoHeaders: true,
|
||||
AllNamespaces: true,
|
||||
},
|
||||
obj: &corev1.Event{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-000",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-12 * time.Minute)),
|
||||
},
|
||||
},
|
||||
expected: `foo 12m (x3 over 20m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
buffer := &bytes.Buffer{}
|
||||
if err := test.printer.PrintObj(test.obj, buffer); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if buffer.String() != test.expected {
|
||||
t.Errorf("\nexpected:\n'%s'\nsaw\n'%s'\n", test.expected, buffer.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -32,12 +32,13 @@ import (
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/cli-runtime/pkg/printers"
|
||||
runtimeresource "k8s.io/cli-runtime/pkg/resource"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
@ -45,10 +46,6 @@ import (
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
const (
|
||||
eventsUsageStr = "events [--for TYPE/NAME] [--watch]"
|
||||
)
|
||||
|
||||
var (
|
||||
eventsLong = templates.LongDesc(i18n.T(`
|
||||
Experimental: Display events
|
||||
@ -65,7 +62,13 @@ var (
|
||||
kubectl alpha events --all-namespaces
|
||||
|
||||
# List recent events for the specified pod, then wait for more events and list them as they arrive.
|
||||
kubectl alpha events --for pod/web-pod-13je7 --watch`))
|
||||
kubectl alpha events --for pod/web-pod-13je7 --watch
|
||||
|
||||
# List recent events in given format. Supported ones, apart from default, are json and yaml.
|
||||
kubectl alpha events -oyaml
|
||||
|
||||
# List recent only events in given event types
|
||||
kubectl alpha events --types=Warning,Normal`))
|
||||
)
|
||||
|
||||
// EventsFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
|
||||
@ -73,10 +76,13 @@ var (
|
||||
// the logic itself easy to unit test.
|
||||
type EventsFlags struct {
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
PrintFlags *genericclioptions.PrintFlags
|
||||
|
||||
AllNamespaces bool
|
||||
Watch bool
|
||||
NoHeaders bool
|
||||
ForObject string
|
||||
FilterTypes []string
|
||||
ChunkSize int64
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
@ -85,6 +91,7 @@ type EventsFlags struct {
|
||||
func NewEventsFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *EventsFlags {
|
||||
return &EventsFlags{
|
||||
RESTClientGetter: restClientGetter,
|
||||
PrintFlags: genericclioptions.NewPrintFlags("events").WithTypeSetter(scheme.Scheme),
|
||||
IOStreams: streams,
|
||||
ChunkSize: cmdutil.DefaultChunkSize,
|
||||
}
|
||||
@ -96,13 +103,15 @@ type EventsOptions struct {
|
||||
Namespace string
|
||||
AllNamespaces bool
|
||||
Watch bool
|
||||
FilterTypes []string
|
||||
|
||||
forGVK schema.GroupVersionKind
|
||||
forName string
|
||||
|
||||
ctx context.Context
|
||||
client *kubernetes.Clientset
|
||||
|
||||
PrintObj printers.ResourcePrinterFunc
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
@ -111,35 +120,39 @@ func NewCmdEvents(restClientGetter genericclioptions.RESTClientGetter, streams g
|
||||
flags := NewEventsFlags(restClientGetter, streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: eventsUsageStr,
|
||||
Use: fmt.Sprintf("events [(-o|--output=)%s] [--for TYPE/NAME] [--watch] [--event=Normal,Warning]", strings.Join(flags.PrintFlags.AllowedFormats(), "|")),
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: i18n.T("Experimental: List events"),
|
||||
Long: eventsLong,
|
||||
Example: eventsExample,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
o, err := flags.ToOptions(cmd.Context(), args)
|
||||
o, err := flags.ToOptions()
|
||||
cmdutil.CheckErr(err)
|
||||
cmdutil.CheckErr(o.Validate())
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
flags.AddFlags(cmd)
|
||||
flags.PrintFlags.AddFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AddFlags registers flags for a cli.
|
||||
func (o *EventsFlags) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVarP(&o.Watch, "watch", "w", o.Watch, "After listing the requested events, watch for more events.")
|
||||
cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
cmd.Flags().StringVar(&o.ForObject, "for", o.ForObject, "Filter events to only those pertaining to the specified resource.")
|
||||
cmdutil.AddChunkSizeFlag(cmd, &o.ChunkSize)
|
||||
func (flags *EventsFlags) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVarP(&flags.Watch, "watch", "w", flags.Watch, "After listing the requested events, watch for more events.")
|
||||
cmd.Flags().BoolVarP(&flags.AllNamespaces, "all-namespaces", "A", flags.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.")
|
||||
cmd.Flags().StringVar(&flags.ForObject, "for", flags.ForObject, "Filter events to only those pertaining to the specified resource.")
|
||||
cmd.Flags().StringSliceVar(&flags.FilterTypes, "types", flags.FilterTypes, "Output only events of given types.")
|
||||
cmd.Flags().BoolVar(&flags.NoHeaders, "no-headers", flags.NoHeaders, "When using the default output format, don't print headers.")
|
||||
cmdutil.AddChunkSizeFlag(cmd, &flags.ChunkSize)
|
||||
}
|
||||
|
||||
// ToOptions converts from CLI inputs to runtime inputs.
|
||||
func (flags *EventsFlags) ToOptions(ctx context.Context, args []string) (*EventsOptions, error) {
|
||||
func (flags *EventsFlags) ToOptions() (*EventsOptions, error) {
|
||||
o := &EventsOptions{
|
||||
ctx: ctx,
|
||||
AllNamespaces: flags.AllNamespaces,
|
||||
Watch: flags.Watch,
|
||||
FilterTypes: flags.FilterTypes,
|
||||
IOStreams: flags.IOStreams,
|
||||
}
|
||||
var err error
|
||||
@ -167,16 +180,46 @@ func (flags *EventsFlags) ToOptions(ctx context.Context, args []string) (*Events
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o.client, err = kubernetes.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(o.FilterTypes) > 0 {
|
||||
o.FilterTypes = sets.NewString(o.FilterTypes...).List()
|
||||
}
|
||||
|
||||
var printer printers.ResourcePrinter
|
||||
if flags.PrintFlags.OutputFormat != nil && len(*flags.PrintFlags.OutputFormat) > 0 {
|
||||
printer, err = flags.PrintFlags.ToPrinter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
printer = NewEventPrinter(flags.NoHeaders, flags.AllNamespaces)
|
||||
}
|
||||
|
||||
o.PrintObj = func(object runtime.Object, writer io.Writer) error {
|
||||
return printer.PrintObj(object, writer)
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func (o *EventsOptions) Validate() error {
|
||||
for _, val := range o.FilterTypes {
|
||||
if !strings.EqualFold(val, "Normal") && !strings.EqualFold(val, "Warning") {
|
||||
return fmt.Errorf("valid --types are Normal or Warning")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Run retrieves events
|
||||
func (o EventsOptions) Run() error {
|
||||
func (o *EventsOptions) Run() error {
|
||||
ctx := context.TODO()
|
||||
namespace := o.Namespace
|
||||
if o.AllNamespaces {
|
||||
namespace = ""
|
||||
@ -188,14 +231,19 @@ func (o EventsOptions) Run() error {
|
||||
fields.OneTermEqualSelector("involvedObject.name", o.forName)).String()
|
||||
}
|
||||
if o.Watch {
|
||||
return o.runWatch(namespace, listOptions)
|
||||
return o.runWatch(ctx, namespace, listOptions)
|
||||
}
|
||||
|
||||
e := o.client.CoreV1().Events(namespace)
|
||||
el := &corev1.EventList{}
|
||||
el := &corev1.EventList{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "EventList",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
}
|
||||
err := runtimeresource.FollowContinue(&listOptions,
|
||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
newEvents, err := e.List(o.ctx, options)
|
||||
newEvents, err := e.List(ctx, options)
|
||||
if err != nil {
|
||||
return nil, runtimeresource.EnhanceListError(err, options, "events")
|
||||
}
|
||||
@ -207,6 +255,22 @@ func (o EventsOptions) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var filteredEvents []corev1.Event
|
||||
for _, e := range el.Items {
|
||||
if !o.filteredEventType(e.Type) {
|
||||
continue
|
||||
}
|
||||
if e.GetObjectKind().GroupVersionKind().Empty() {
|
||||
e.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Version: "v1",
|
||||
Kind: "Event",
|
||||
})
|
||||
}
|
||||
filteredEvents = append(filteredEvents, e)
|
||||
}
|
||||
|
||||
el.Items = filteredEvents
|
||||
|
||||
if len(el.Items) == 0 {
|
||||
if o.AllNamespaces {
|
||||
fmt.Fprintln(o.ErrOut, "No events found.")
|
||||
@ -220,36 +284,39 @@ func (o EventsOptions) Run() error {
|
||||
|
||||
sort.Sort(SortableEvents(el.Items))
|
||||
|
||||
printHeadings(w, o.AllNamespaces)
|
||||
for _, e := range el.Items {
|
||||
printOneEvent(w, e, o.AllNamespaces)
|
||||
}
|
||||
o.PrintObj(el, w)
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o EventsOptions) runWatch(namespace string, listOptions metav1.ListOptions) error {
|
||||
eventWatch, err := o.client.CoreV1().Events(namespace).Watch(o.ctx, listOptions)
|
||||
func (o *EventsOptions) runWatch(ctx context.Context, namespace string, listOptions metav1.ListOptions) error {
|
||||
eventWatch, err := o.client.CoreV1().Events(namespace).Watch(ctx, listOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := printers.GetNewTabWriter(o.Out)
|
||||
headingsPrinted := false
|
||||
|
||||
ctx, cancel := context.WithCancel(o.ctx)
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
intr := interrupt.New(nil, cancel)
|
||||
intr.Run(func() error {
|
||||
_, err := watchtools.UntilWithoutRetry(ctx, eventWatch, func(e watch.Event) (bool, error) {
|
||||
_, err := watchtools.UntilWithoutRetry(cctx, eventWatch, func(e watch.Event) (bool, error) {
|
||||
if e.Type == watch.Deleted { // events are deleted after 1 hour; don't print that
|
||||
return false, nil
|
||||
}
|
||||
event := e.Object.(*corev1.Event)
|
||||
if !headingsPrinted {
|
||||
printHeadings(w, o.AllNamespaces)
|
||||
headingsPrinted = true
|
||||
|
||||
if ev, ok := e.Object.(*corev1.Event); !ok || !o.filteredEventType(ev.Type) {
|
||||
return false, nil
|
||||
}
|
||||
printOneEvent(w, *event, o.AllNamespaces)
|
||||
|
||||
if e.Object.GetObjectKind().GroupVersionKind().Empty() {
|
||||
e.Object.GetObjectKind().SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Version: "v1",
|
||||
Kind: "Event",
|
||||
})
|
||||
}
|
||||
|
||||
o.PrintObj(e.Object, w)
|
||||
w.Flush()
|
||||
return false, nil
|
||||
})
|
||||
@ -259,36 +326,22 @@ func (o EventsOptions) runWatch(namespace string, listOptions metav1.ListOptions
|
||||
return nil
|
||||
}
|
||||
|
||||
func printHeadings(w io.Writer, allNamespaces bool) {
|
||||
if allNamespaces {
|
||||
fmt.Fprintf(w, "NAMESPACE\t")
|
||||
// filteredEventType checks given event can be printed
|
||||
// by comparing it in filtered event flag.
|
||||
// If --event flag is not set by user, this function allows
|
||||
// all events to be printed.
|
||||
func (o *EventsOptions) filteredEventType(et string) bool {
|
||||
if len(o.FilterTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
fmt.Fprintf(w, "LAST SEEN\tTYPE\tREASON\tOBJECT\tMESSAGE\n")
|
||||
}
|
||||
|
||||
func printOneEvent(w io.Writer, e corev1.Event, allNamespaces bool) {
|
||||
var interval string
|
||||
firstTimestampSince := translateMicroTimestampSince(e.EventTime)
|
||||
if e.EventTime.IsZero() {
|
||||
firstTimestampSince = translateTimestampSince(e.FirstTimestamp)
|
||||
for _, t := range o.FilterTypes {
|
||||
if strings.EqualFold(t, et) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if e.Series != nil {
|
||||
interval = fmt.Sprintf("%s (x%d over %s)", translateMicroTimestampSince(e.Series.LastObservedTime), e.Series.Count, firstTimestampSince)
|
||||
} else if e.Count > 1 {
|
||||
interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, firstTimestampSince)
|
||||
} else {
|
||||
interval = firstTimestampSince
|
||||
}
|
||||
if allNamespaces {
|
||||
fmt.Fprintf(w, "%v\t", e.Namespace)
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s/%s\t%v\n",
|
||||
interval,
|
||||
e.Type,
|
||||
e.Reason,
|
||||
e.InvolvedObject.Kind, e.InvolvedObject.Name,
|
||||
strings.TrimSpace(e.Message),
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SortableEvents implements sort.Interface for []api.Event by time
|
||||
@ -318,26 +371,6 @@ func eventTime(event corev1.Event) time.Time {
|
||||
return event.EventTime.Time
|
||||
}
|
||||
|
||||
// translateMicroTimestampSince returns the elapsed time since timestamp in
|
||||
// human-readable approximation.
|
||||
func translateMicroTimestampSince(timestamp metav1.MicroTime) string {
|
||||
if timestamp.IsZero() {
|
||||
return "<unknown>"
|
||||
}
|
||||
|
||||
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||
}
|
||||
|
||||
// translateTimestampSince returns the elapsed time since timestamp in
|
||||
// human-readable approximation.
|
||||
func translateTimestampSince(timestamp metav1.Time) string {
|
||||
if timestamp.IsZero() {
|
||||
return "<unknown>"
|
||||
}
|
||||
|
||||
return duration.HumanDuration(time.Since(timestamp.Time))
|
||||
}
|
||||
|
||||
// Inspired by k8s.io/cli-runtime/pkg/resource splitResourceTypeName()
|
||||
|
||||
// decodeResourceTypeName handles type/name resource formats and returns a resource tuple
|
||||
|
219
staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go
Normal file
219
staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go
Normal file
@ -0,0 +1,219 @@
|
||||
/*
|
||||
Copyright 2022 The Kubernetes 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 events
|
||||
|
||||
import (
|
||||
"io"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest/fake"
|
||||
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
|
||||
)
|
||||
|
||||
func getFakeEvents() *corev1.EventList {
|
||||
return &corev1.EventList{
|
||||
Items: []corev1.Event{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-000",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-30 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-20 * time.Minute)),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-001",
|
||||
Namespace: "foo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "foo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeWarning,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-28 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-18 * time.Minute)),
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "bar-002",
|
||||
Namespace: "otherfoo",
|
||||
},
|
||||
InvolvedObject: corev1.ObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: "bar",
|
||||
Namespace: "otherfoo",
|
||||
UID: "00000000-0000-0000-0000-000000000001",
|
||||
},
|
||||
Type: corev1.EventTypeNormal,
|
||||
Reason: "ScalingReplicaSet",
|
||||
Message: "Scaled up replica set bar-002 to 1",
|
||||
ReportingController: "deployment-controller",
|
||||
EventTime: metav1.NewMicroTime(time.Now().Add(-25 * time.Minute)),
|
||||
Series: &corev1.EventSeries{
|
||||
Count: 3,
|
||||
LastObservedTime: metav1.NewMicroTime(time.Now().Add(-15 * time.Minute)),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventIsSorted(t *testing.T) {
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
|
||||
if err != err {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getFakeEvents())}, nil
|
||||
})
|
||||
|
||||
printer := NewEventPrinter(false, true)
|
||||
|
||||
options := &EventsOptions{
|
||||
AllNamespaces: true,
|
||||
client: clientset,
|
||||
PrintObj: func(object runtime.Object, writer io.Writer) error {
|
||||
return printer.PrintObj(object, writer)
|
||||
},
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
err = options.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
`
|
||||
if e, a := expected, buf.String(); e != a {
|
||||
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventNoHeaders(t *testing.T) {
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
|
||||
if err != err {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getFakeEvents())}, nil
|
||||
})
|
||||
|
||||
printer := NewEventPrinter(true, true)
|
||||
|
||||
options := &EventsOptions{
|
||||
AllNamespaces: true,
|
||||
client: clientset,
|
||||
PrintObj: func(object runtime.Object, writer io.Writer) error {
|
||||
return printer.PrintObj(object, writer)
|
||||
},
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
err = options.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `foo 20m (x3 over 30m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
otherfoo 15m (x3 over 25m) Normal ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
`
|
||||
if e, a := expected, buf.String(); e != a {
|
||||
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventFiltered(t *testing.T) {
|
||||
codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
|
||||
streams, _, buf, _ := genericclioptions.NewTestIOStreams()
|
||||
clientset, err := kubernetes.NewForConfig(cmdtesting.DefaultClientConfig())
|
||||
if err != err {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientset.CoreV1().RESTClient().(*restclient.RESTClient).Client = fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, getFakeEvents())}, nil
|
||||
})
|
||||
|
||||
printer := NewEventPrinter(false, true)
|
||||
|
||||
options := &EventsOptions{
|
||||
AllNamespaces: true,
|
||||
client: clientset,
|
||||
FilterTypes: []string{"WARNING"},
|
||||
PrintObj: func(object runtime.Object, writer io.Writer) error {
|
||||
return printer.PrintObj(object, writer)
|
||||
},
|
||||
IOStreams: streams,
|
||||
}
|
||||
|
||||
err = options.Run()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := `NAMESPACE LAST SEEN TYPE REASON OBJECT MESSAGE
|
||||
foo 18m (x3 over 28m) Warning ScalingReplicaSet Deployment/bar Scaled up replica set bar-002 to 1
|
||||
`
|
||||
if e, a := expected, buf.String(); e != a {
|
||||
t.Errorf("expected\n%v\ngot\n%v", e, a)
|
||||
}
|
||||
}
|
@ -61,6 +61,27 @@ run_kubectl_events_tests() {
|
||||
output_message=$(kubectl alpha events -n test-events --for=Cronjob/pi --watch --request-timeout=1 "${kube_flags[@]:?}" 2>&1)
|
||||
kube::test::if_has_string "${output_message}" "Warning" "InvalidSchedule" "Cronjob/pi"
|
||||
|
||||
# Post-Condition: events returns event for Cronjob/pi when filtered by Warning
|
||||
output_message=$(kubectl alpha events -n test-events --for=Cronjob/pi --types=Warning "${kube_flags[@]:?}" 2>&1)
|
||||
kube::test::if_has_string "${output_message}" "Warning" "InvalidSchedule" "Cronjob/pi"
|
||||
|
||||
# Post-Condition: events not returns event for Cronjob/pi when filtered only by Normal
|
||||
output_message=$(kubectl alpha events -n test-events --for=Cronjob/pi --types=Normal "${kube_flags[@]:?}" 2>&1)
|
||||
kube::test::if_has_not_string "${output_message}" "Warning" "InvalidSchedule" "Cronjob/pi"
|
||||
|
||||
# Post-Condition: events returns event for Cronjob/pi without headers
|
||||
output_message=$(kubectl alpha events -n test-events --for=Cronjob/pi --no-headers "${kube_flags[@]:?}" 2>&1)
|
||||
kube::test::if_has_not_string "${output_message}" "LAST SEEN" "TYPE" "REASON"
|
||||
kube::test::if_has_string "${output_message}" "Warning" "InvalidSchedule" "Cronjob/pi"
|
||||
|
||||
# Post-Condition: events returns event for Cronjob/pi in json format
|
||||
output_message=$(kubectl alpha events -n test-events --for=Cronjob/pi --output=json "${kube_flags[@]:?}" 2>&1)
|
||||
kube::test::if_has_string "${output_message}" "Warning" "InvalidSchedule" "Cronjob/pi"
|
||||
|
||||
# Post-Condition: events returns event for Cronjob/pi in yaml format
|
||||
output_message=$(kubectl alpha events -n test-events --for=Cronjob/pi --output=yaml "${kube_flags[@]:?}" 2>&1)
|
||||
kube::test::if_has_string "${output_message}" "Warning" "InvalidSchedule" "Cronjob/pi"
|
||||
|
||||
#Clean up
|
||||
kubectl delete cronjob pi --namespace=test-events
|
||||
kubectl delete namespace test-events
|
||||
|
Loading…
Reference in New Issue
Block a user