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:
Arda Güçlü 2022-05-10 13:09:59 +03:00
parent ce433f87b4
commit 63b8684cd3
5 changed files with 704 additions and 81 deletions

View 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,
}
}

View 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())
}
})
}
}

View File

@ -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

View 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)
}
}

View File

@ -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