mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
'kubectl events' command (#99557)
* Basic scaffolding for kubectl events command * Simple implementation of 'kubectl events' * Print event fields Borrowing code from 'kubectl describe' * Sort events before printing * Make output more like 'kubectl get events' Print namespace if fetching from all * Refactor: extract print functions To make it easier to add watch function next. * Implement watch for new events Run the list function same as before, so those events come out in order, then watch from the ResourceVersion after the last event. * Clear up some lint warnings * Update description and example text * refactor: introduce a listOptions variable * Implement --for option splitResourceTypeName() function borrowed from cli-runtime because we need similar behaviour to Builder.ResourceNames(), but applying to Event.InvolvedObject. * Add kubectl/pkg/cmd/events to vendor/modules.txt * Add back cmdutil.Factory Needed in NewCmdAlpha to construct the 'events' command Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Fix lint Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Make package alias go-cromulent Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Use genericclioptions.RESTClientGetter instead of cmdutil.Factory Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Decode object parameter to GroupVersionKind and name Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Merge two files into one Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Include example for --for option Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Fix inconsistent option name in error message Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Remove unnecessary Validate call Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Use fields from event series Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Support chunking of list of events Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Use watchtools utility to receive watch events Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Split struct into flags and options Flags represents the inputs from the CLI; options are what the running command needs. This structure parallels the 'wait' command, and should be easier to write tests for. Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Add some parameters to usage string Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Do only one of list or watch We tried to print the list first then start a watch after the last event, but sometimes the server will return "watch is too old" on that ResourceVersion. Instead, if we need to watch just start it from the beginning. Signed-off-by: Bryan Boreham <bjboreham@gmail.com> * Print message if no events found Also, if watching, don't print the headings until the first event arrives. Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
This commit is contained in:
parent
e580aa93d1
commit
a988182f59
@ -20,12 +20,14 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
"k8s.io/kubectl/pkg/cmd/events"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
// NewCmdAlpha creates a command that acts as an alternate root command for features in alpha
|
||||
func NewCmdAlpha(streams genericclioptions.IOStreams) *cobra.Command {
|
||||
func NewCmdAlpha(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "alpha",
|
||||
Short: i18n.T("Commands for features in alpha"),
|
||||
@ -34,6 +36,7 @@ func NewCmdAlpha(streams genericclioptions.IOStreams) *cobra.Command {
|
||||
|
||||
// Alpha commands should be added here. As features graduate from alpha they should move
|
||||
// from here to the CommandGroups defined by NewKubeletCommand() in cmd.go.
|
||||
cmd.AddCommand(events.NewCmdEvents(f, streams))
|
||||
|
||||
// NewKubeletCommand() will hide the alpha command if it has no subcommands. Overriding
|
||||
// the help function ensures a reasonable message if someone types the hidden command anyway.
|
||||
|
@ -382,7 +382,7 @@ func NewKubectlCommand(in io.Reader, out, err io.Writer) *cobra.Command {
|
||||
filters := []string{"options"}
|
||||
|
||||
// Hide the "alpha" subcommand if there are no alpha commands in this build.
|
||||
alpha := NewCmdAlpha(ioStreams)
|
||||
alpha := NewCmdAlpha(f, ioStreams)
|
||||
if !alpha.HasSubCommands() {
|
||||
filters = append(filters, alpha.Name())
|
||||
}
|
||||
|
368
staging/src/k8s.io/kubectl/pkg/cmd/events/events.go
Normal file
368
staging/src/k8s.io/kubectl/pkg/cmd/events/events.go
Normal file
@ -0,0 +1,368 @@
|
||||
/*
|
||||
Copyright 2021 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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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/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"
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
||||
"k8s.io/kubectl/pkg/util/i18n"
|
||||
"k8s.io/kubectl/pkg/util/interrupt"
|
||||
"k8s.io/kubectl/pkg/util/templates"
|
||||
)
|
||||
|
||||
const (
|
||||
eventsUsageStr = "events [--for TYPE/NAME] [--watch]"
|
||||
)
|
||||
|
||||
var (
|
||||
eventsLong = templates.LongDesc(i18n.T(`
|
||||
Experimental: Display events
|
||||
|
||||
Prints a table of the most important information about events.
|
||||
You can request events for a namespace, for all namespace, or
|
||||
filtered to only those pertaining to a specified resource.`))
|
||||
|
||||
eventsExample = templates.Examples(i18n.T(`
|
||||
# List recent events in the default namespace.
|
||||
kubectl alpha events
|
||||
|
||||
# List recent events in all namespaces.
|
||||
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`))
|
||||
)
|
||||
|
||||
// EventsFlags directly reflect the information that CLI is gathering via flags. They will be converted to Options, which
|
||||
// reflect the runtime requirements for the command. This structure reduces the transformation to wiring and makes
|
||||
// the logic itself easy to unit test.
|
||||
type EventsFlags struct {
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
|
||||
AllNamespaces bool
|
||||
Watch bool
|
||||
ForObject string
|
||||
ChunkSize int64
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewEventsFlags returns a default EventsFlags
|
||||
func NewEventsFlags(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *EventsFlags {
|
||||
return &EventsFlags{
|
||||
RESTClientGetter: restClientGetter,
|
||||
IOStreams: streams,
|
||||
ChunkSize: cmdutil.DefaultChunkSize,
|
||||
}
|
||||
}
|
||||
|
||||
// EventsOptions is a set of options that allows you to list events. This is the object reflects the
|
||||
// runtime needs of an events command, making the logic itself easy to unit test.
|
||||
type EventsOptions struct {
|
||||
Namespace string
|
||||
AllNamespaces bool
|
||||
Watch bool
|
||||
|
||||
forGVK schema.GroupVersionKind
|
||||
forName string
|
||||
|
||||
ctx context.Context
|
||||
client *kubernetes.Clientset
|
||||
|
||||
genericclioptions.IOStreams
|
||||
}
|
||||
|
||||
// NewCmdEvents creates a new events command
|
||||
func NewCmdEvents(restClientGetter genericclioptions.RESTClientGetter, streams genericclioptions.IOStreams) *cobra.Command {
|
||||
flags := NewEventsFlags(restClientGetter, streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: eventsUsageStr,
|
||||
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)
|
||||
cmdutil.CheckErr(err)
|
||||
cmdutil.CheckErr(o.Run())
|
||||
},
|
||||
}
|
||||
flags.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)
|
||||
}
|
||||
|
||||
// ToOptions converts from CLI inputs to runtime inputs.
|
||||
func (flags *EventsFlags) ToOptions(ctx context.Context, args []string) (*EventsOptions, error) {
|
||||
o := &EventsOptions{
|
||||
ctx: ctx,
|
||||
AllNamespaces: flags.AllNamespaces,
|
||||
Watch: flags.Watch,
|
||||
IOStreams: flags.IOStreams,
|
||||
}
|
||||
var err error
|
||||
o.Namespace, _, err = flags.RESTClientGetter.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if flags.ForObject != "" {
|
||||
mapper, err := flags.RESTClientGetter.ToRESTMapper()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var found bool
|
||||
o.forGVK, o.forName, found, err = decodeResourceTypeName(mapper, flags.ForObject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("--for must be in resource/name form")
|
||||
}
|
||||
}
|
||||
|
||||
clientConfig, err := flags.RESTClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o.client, err = kubernetes.NewForConfig(clientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Run retrieves events
|
||||
func (o EventsOptions) Run() error {
|
||||
namespace := o.Namespace
|
||||
if o.AllNamespaces {
|
||||
namespace = ""
|
||||
}
|
||||
listOptions := metav1.ListOptions{Limit: cmdutil.DefaultChunkSize}
|
||||
if o.forName != "" {
|
||||
listOptions.FieldSelector = fields.AndSelectors(
|
||||
fields.OneTermEqualSelector("involvedObject.kind", o.forGVK.Kind),
|
||||
fields.OneTermEqualSelector("involvedObject.name", o.forName)).String()
|
||||
}
|
||||
if o.Watch {
|
||||
return o.runWatch(namespace, listOptions)
|
||||
}
|
||||
|
||||
e := o.client.CoreV1().Events(namespace)
|
||||
el := &corev1.EventList{}
|
||||
err := runtimeresource.FollowContinue(&listOptions,
|
||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||
newEvents, err := e.List(o.ctx, options)
|
||||
if err != nil {
|
||||
return nil, runtimeresource.EnhanceListError(err, options, "events")
|
||||
}
|
||||
el.Items = append(el.Items, newEvents.Items...)
|
||||
return newEvents, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(el.Items) == 0 {
|
||||
if o.AllNamespaces {
|
||||
fmt.Fprintln(o.ErrOut, "No events found.")
|
||||
} else {
|
||||
fmt.Fprintf(o.ErrOut, "No events found in %s namespace.\n", o.Namespace)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
w := printers.GetNewTabWriter(o.Out)
|
||||
|
||||
sort.Sort(SortableEvents(el.Items))
|
||||
|
||||
printHeadings(w, o.AllNamespaces)
|
||||
for _, e := range el.Items {
|
||||
printOneEvent(w, e, o.AllNamespaces)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w := printers.GetNewTabWriter(o.Out)
|
||||
headingsPrinted := false
|
||||
|
||||
ctx, cancel := context.WithCancel(o.ctx)
|
||||
defer cancel()
|
||||
intr := interrupt.New(nil, cancel)
|
||||
intr.Run(func() error {
|
||||
_, err := watchtools.UntilWithoutRetry(ctx, 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
|
||||
}
|
||||
printOneEvent(w, *event, o.AllNamespaces)
|
||||
w.Flush()
|
||||
return false, nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printHeadings(w io.Writer, allNamespaces bool) {
|
||||
if allNamespaces {
|
||||
fmt.Fprintf(w, "NAMESPACE\t")
|
||||
}
|
||||
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)
|
||||
}
|
||||
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),
|
||||
)
|
||||
}
|
||||
|
||||
// SortableEvents implements sort.Interface for []api.Event by time
|
||||
type SortableEvents []corev1.Event
|
||||
|
||||
func (list SortableEvents) Len() int {
|
||||
return len(list)
|
||||
}
|
||||
|
||||
func (list SortableEvents) Swap(i, j int) {
|
||||
list[i], list[j] = list[j], list[i]
|
||||
}
|
||||
|
||||
func (list SortableEvents) Less(i, j int) bool {
|
||||
return eventTime(list[i]).Before(eventTime(list[j]))
|
||||
}
|
||||
|
||||
// Return the time that should be used for sorting, which can come from
|
||||
// various places in corev1.Event.
|
||||
func eventTime(event corev1.Event) time.Time {
|
||||
if event.Series != nil {
|
||||
return event.Series.LastObservedTime.Time
|
||||
}
|
||||
if !event.LastTimestamp.Time.IsZero() {
|
||||
return event.LastTimestamp.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
|
||||
// (empty or not), whether it successfully found one, and an error
|
||||
func decodeResourceTypeName(mapper meta.RESTMapper, s string) (gvk schema.GroupVersionKind, name string, found bool, err error) {
|
||||
if !strings.Contains(s, "/") {
|
||||
return
|
||||
}
|
||||
seg := strings.Split(s, "/")
|
||||
if len(seg) != 2 {
|
||||
err = fmt.Errorf("arguments in resource/name form may not have more than one slash")
|
||||
return
|
||||
}
|
||||
resource, name := seg[0], seg[1]
|
||||
|
||||
var gvr schema.GroupVersionResource
|
||||
gvr, err = mapper.ResourceFor(schema.GroupVersionResource{Resource: resource})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gvk, err = mapper.KindFor(gvr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
found = true
|
||||
|
||||
return
|
||||
}
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -2104,6 +2104,7 @@ k8s.io/kubectl/pkg/cmd/describe
|
||||
k8s.io/kubectl/pkg/cmd/diff
|
||||
k8s.io/kubectl/pkg/cmd/drain
|
||||
k8s.io/kubectl/pkg/cmd/edit
|
||||
k8s.io/kubectl/pkg/cmd/events
|
||||
k8s.io/kubectl/pkg/cmd/exec
|
||||
k8s.io/kubectl/pkg/cmd/explain
|
||||
k8s.io/kubectl/pkg/cmd/expose
|
||||
|
Loading…
Reference in New Issue
Block a user