diff --git a/go.mod b/go.mod index 7ee161e39..a4295fc8e 100644 --- a/go.mod +++ b/go.mod @@ -31,10 +31,10 @@ require ( github.com/stretchr/testify v1.7.0 golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.0.0-20211028163534-c9a65ff2d94d - k8s.io/apimachinery v0.0.0-20211027003259-a39cb4b7e43a + k8s.io/api v0.0.0-20211029083603-41019181ea88 + k8s.io/apimachinery v0.0.0-20211028185107-b255da54548a k8s.io/cli-runtime v0.0.0-20211027005851-fd0a6d95140a - k8s.io/client-go v0.0.0-20211028163848-c151c2c68885 + k8s.io/client-go v0.0.0-20211029083953-5be956ba48bd k8s.io/component-base v0.0.0-20211027004438-bd08cb7812c3 k8s.io/component-helpers v0.0.0-20211027004542-73bcdef827e4 k8s.io/klog/v2 v2.30.0 @@ -47,10 +47,10 @@ require ( ) replace ( - k8s.io/api => k8s.io/api v0.0.0-20211028163534-c9a65ff2d94d - k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20211027003259-a39cb4b7e43a + k8s.io/api => k8s.io/api v0.0.0-20211029083603-41019181ea88 + k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20211028185107-b255da54548a k8s.io/cli-runtime => k8s.io/cli-runtime v0.0.0-20211027005851-fd0a6d95140a - k8s.io/client-go => k8s.io/client-go v0.0.0-20211028163848-c151c2c68885 + k8s.io/client-go => k8s.io/client-go v0.0.0-20211029083953-5be956ba48bd k8s.io/code-generator => k8s.io/code-generator v0.0.0-20211026222709-e92ab9f4d5a1 k8s.io/component-base => k8s.io/component-base v0.0.0-20211027004438-bd08cb7812c3 k8s.io/component-helpers => k8s.io/component-helpers v0.0.0-20211027004542-73bcdef827e4 diff --git a/go.sum b/go.sum index 7cd471fab..f026878b3 100644 --- a/go.sum +++ b/go.sum @@ -902,14 +902,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.0.0-20211028163534-c9a65ff2d94d h1:Co3m8PMBF03sz+T3rZNKjszQGE84So/b/N77Jni1AJw= -k8s.io/api v0.0.0-20211028163534-c9a65ff2d94d/go.mod h1:FBKdg4CgNljGEmNwAg4dU/YOL9216JaN8EMrLzRcpvk= -k8s.io/apimachinery v0.0.0-20211027003259-a39cb4b7e43a h1:0ipBhp8NPPsCxYT+zL42GEB+f7Tc+82+vCkOoYdr8Fg= -k8s.io/apimachinery v0.0.0-20211027003259-a39cb4b7e43a/go.mod h1:oyH3LcOKLLooQH1NlpHlilzkWxqsiHWETyHgssntcXg= +k8s.io/api v0.0.0-20211029083603-41019181ea88 h1:fNrtAsJFqgYBBCed0914pfMwaOUnYg8Ygl43m5Ow+RM= +k8s.io/api v0.0.0-20211029083603-41019181ea88/go.mod h1:dx3B5TOvDwqR2Oqn8muvHae5vt+xIgmxap/fvP+iItQ= +k8s.io/apimachinery v0.0.0-20211028185107-b255da54548a h1:3NA1KMmF0ie/tLSNnA3v1ihGjVX7Z5OmZIyAOm0YmVo= +k8s.io/apimachinery v0.0.0-20211028185107-b255da54548a/go.mod h1:oyH3LcOKLLooQH1NlpHlilzkWxqsiHWETyHgssntcXg= k8s.io/cli-runtime v0.0.0-20211027005851-fd0a6d95140a h1:6N6d0a80cFO2PI8RssKTsofnWWdzr4LBWKKe0t+1kAk= k8s.io/cli-runtime v0.0.0-20211027005851-fd0a6d95140a/go.mod h1:C/9bEqryTALzT3y/AJFeg2TqzudoaRZrdr4Zdoeetn0= -k8s.io/client-go v0.0.0-20211028163848-c151c2c68885 h1:xOnhauM+e4MqqNHxUzZ9xcdOw9ReLZ802bIKJiFO04Q= -k8s.io/client-go v0.0.0-20211028163848-c151c2c68885/go.mod h1:UqjftQM30eBDmlJ/Ef0bhOiNV0yTVqazKm9rUnikyM8= +k8s.io/client-go v0.0.0-20211029083953-5be956ba48bd h1:k4VNvA+D6bQXpz5bal2Bwrn2aZ3CBg9I+3P2xPztjhs= +k8s.io/client-go v0.0.0-20211029083953-5be956ba48bd/go.mod h1:/Cx6iv3iME4npCXzwBmPx9YFW9YvuLpEUzzreR/3EsM= k8s.io/code-generator v0.0.0-20211026222709-e92ab9f4d5a1/go.mod h1:alK4pz5+y/zKXOPBnND3TvXOC/iF2oYTBDynHO1+qlI= k8s.io/component-base v0.0.0-20211027004438-bd08cb7812c3 h1:Hx2Olj7sA4W9r8veeamsvNiiYf5SMTN/N/1uAMQk13c= k8s.io/component-base v0.0.0-20211027004438-bd08cb7812c3/go.mod h1:7xqZFY7A2hOLqh+cOR9jinp+xJVXp6F5I1rOO+kBKJc= diff --git a/pkg/cmd/alpha.go b/pkg/cmd/alpha.go index f606421ce..13b081aac 100644 --- a/pkg/cmd/alpha.go +++ b/pkg/cmd/alpha.go @@ -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. diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 5c13f51fb..fde8882a2 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -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()) } diff --git a/pkg/cmd/events/events.go b/pkg/cmd/events/events.go new file mode 100644 index 000000000..5f76db2b9 --- /dev/null +++ b/pkg/cmd/events/events.go @@ -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 "" + } + + 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 +// (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 +}