diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go b/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go new file mode 100644 index 00000000000..b9254bb7997 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer.go @@ -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 "" + } + + 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 "" + } + + return duration.HumanDuration(time.Since(timestamp.Time)) +} + +func NewEventPrinter(noHeader, allNamespaces bool) *EventPrinter { + return &EventPrinter{ + NoHeaders: noHeader, + AllNamespaces: allNamespaces, + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go new file mode 100644 index 00000000000..c52eae87efe --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/events/event_printer_test.go @@ -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()) + } + }) + } +} diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/events/events.go b/staging/src/k8s.io/kubectl/pkg/cmd/events/events.go index 5f76db2b9af..78bf33cb50f 100644 --- a/staging/src/k8s.io/kubectl/pkg/cmd/events/events.go +++ b/staging/src/k8s.io/kubectl/pkg/cmd/events/events.go @@ -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 "" - } - - 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 "" - } - - 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 diff --git a/staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go b/staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go new file mode 100644 index 00000000000..180c1da48f1 --- /dev/null +++ b/staging/src/k8s.io/kubectl/pkg/cmd/events/events_test.go @@ -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) + } +} diff --git a/test/cmd/events.sh b/test/cmd/events.sh index b9b02ae7278..2d1163c7eac 100755 --- a/test/cmd/events.sh +++ b/test/cmd/events.sh @@ -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