From 5b18289808260ff079586a0e83ef71fbdeb40e3b Mon Sep 17 00:00:00 2001 From: Somtochi Onyekwere Date: Wed, 22 Feb 2023 19:30:53 +0100 Subject: [PATCH 1/6] Add `flux events` command This adds a command to Flux which behaves similarly as `kubectl events`, including the Flux sources events when showing events for top level objects, thus making debugging easy for Flux users. For example, `flux events --for kustomization/` includes the events of its source (e.g. a `GitRepository`, `OCIRepository` or `Bucket`). In addition, `flux events --for helmrelease/` includes events of the `HelmChart` and `HelmRepository`. While `flux events --for alerts/` includes the events of the `Provider`. Signed-off-by: Somtochi Onyekwere --- cmd/flux/events.go | 490 ++++++++++++++++++++++++++++++++++++++++ cmd/flux/events_test.go | 411 +++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 4 files changed, 904 insertions(+) create mode 100644 cmd/flux/events.go create mode 100644 cmd/flux/events_test.go diff --git a/cmd/flux/events.go b/cmd/flux/events.go new file mode 100644 index 0000000000..6f9bba0592 --- /dev/null +++ b/cmd/flux/events.go @@ -0,0 +1,490 @@ +/* +Copyright 2023 The Kubernetes Authors. +Copyright 2023 The Flux 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 main + +import ( + "context" + "fmt" + "os" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/apimachinery/pkg/watch" + runtimeresource "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" + + helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/pkg/printers" +) + +var eventsCmd = &cobra.Command{ + Use: "events", + Short: "Display Kubernetes events for Flux resources", + Long: "The events sub-command shows Kubernetes events from Flux resources", + Example: ` # Display events for flux resources in default namespace + flux events -n default + + # Display events for flux resources in all namespaces + flux events -A + + # Display events for flux resources + flux events --for Kustomization/podinfo +`, + RunE: eventsCmdRun, +} + +type eventFlags struct { + allNamespaces bool + watch bool + forSelector string + filterTypes []string +} + +var eventArgs eventFlags + +func init() { + eventsCmd.Flags().BoolVarP(&eventArgs.allNamespaces, "all-namespaces", "A", false, + "display events from Flux resources across all namespaces") + eventsCmd.Flags().BoolVarP(&eventArgs.watch, "watch", "w", false, + "indicate if the events should be streamed") + eventsCmd.Flags().StringVar(&eventArgs.forSelector, "for", "", + "get events for a particular object") + eventsCmd.Flags().StringSliceVar(&eventArgs.filterTypes, "types", []string{}, "filter events for certain types") + rootCmd.AddCommand(eventsCmd) +} + +func eventsCmdRun(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeclient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + namespace := *kubeconfigArgs.Namespace + if eventArgs.allNamespaces { + namespace = "" + } + + var diffRefNs bool + clientListOpts := getListOpt(namespace, eventArgs.forSelector) + var refListOpts [][]client.ListOption + if eventArgs.forSelector != "" { + refs, err := getObjectRef(ctx, kubeclient, eventArgs.forSelector, *kubeconfigArgs.Namespace) + if err != nil { + return err + } + + for _, ref := range refs { + kind, name, refNs := utils.ParseObjectKindNameNamespace(ref) + if refNs != namespace { + diffRefNs = true + } + refSelector := fmt.Sprintf("%s/%s", kind, name) + refListOpts = append(refListOpts, getListOpt(refNs, refSelector)) + } + } + + showNamespace := namespace == "" || diffRefNs + if eventArgs.watch { + return eventsCmdWatchRun(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) + } + + rows, err := getRows(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) + if len(rows) == 0 { + if eventArgs.allNamespaces { + logger.Failuref("No flux events found.") + } else { + logger.Failuref("No flux events found in %s namespace.\n", *kubeconfigArgs.Namespace) + } + + return nil + } + headers := getHeaders(showNamespace) + err = printers.TablePrinter(headers).Print(cmd.OutOrStdout(), rows) + return err +} + +func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) ([][]string, error) { + el := &corev1.EventList{} + if err := addEventsToList(ctx, kubeclient, el, clientListOpts); err != nil { + return nil, err + } + + for _, refOpts := range refListOpts { + if err := addEventsToList(ctx, kubeclient, el, refOpts); err != nil { + return nil, err + } + } + + sort.Sort(SortableEvents(el.Items)) + + var rows [][]string + for _, item := range el.Items { + if filterEvent(item) { + continue + } + rows = append(rows, getEventRow(item, showNs)) + } + + return rows, nil +} + +func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error { + listOpts := &metav1.ListOptions{} + err := runtimeresource.FollowContinue(listOpts, + func(options metav1.ListOptions) (runtime.Object, error) { + newEvents := &corev1.EventList{} + err := kubeclient.List(ctx, newEvents, clientListOpts...) + if err != nil { + return nil, fmt.Errorf("error getting events: %w", err) + } + el.Items = append(el.Items, newEvents.Items...) + return newEvents, nil + }) + + return err +} + +func getListOpt(namespace, selector string) []client.ListOption { + clientListOpts := []client.ListOption{client.Limit(cmdutil.DefaultChunkSize), client.InNamespace(namespace)} + if selector != "" { + kind, name := utils.ParseObjectKindName(selector) + sel := fields.AndSelectors( + fields.OneTermEqualSelector("involvedObject.kind", kind), + fields.OneTermEqualSelector("involvedObject.name", name)) + clientListOpts = append(clientListOpts, client.MatchingFieldsSelector{Selector: sel}) + } + + return clientListOpts +} + +func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) error { + event := &corev1.EventList{} + eventWatch, err := kubeclient.Watch(ctx, event, listOpts...) + if err != nil { + return err + } + defer eventWatch.Stop() + + firstIteration := true + + handleEvent := func(e watch.Event) error { + if e.Type == watch.Deleted { + return nil + } + + event, ok := e.Object.(*corev1.Event) + if !ok { + return nil + } + if filterEvent(*event) { + return nil + } + rows := getEventRow(*event, showNs) + var hdr []string + if firstIteration { + hdr = getHeaders(showNs) + firstIteration = false + } + err = printers.TablePrinter(hdr).Print(os.Stdout, [][]string{rows}) + if err != nil { + return err + } + + return nil + } + + for _, refOpts := range refListOpts { + refEventWatch, err := kubeclient.Watch(ctx, event, refOpts...) + if err != nil { + return err + } + defer refEventWatch.Stop() + go receiveEventChan(ctx, refEventWatch, handleEvent) + } + + return receiveEventChan(ctx, eventWatch, handleEvent) + +} + +func receiveEventChan(ctx context.Context, eventWatch watch.Interface, f func(e watch.Event) error) error { + for { + select { + case e, ok := <-eventWatch.ResultChan(): + if !ok { + return nil + } + err := f(e) + if err != nil { + return err + } + case <-ctx.Done(): + return nil + } + } +} + +func getHeaders(showNs bool) []string { + headers := []string{"Last seen", "Type", "Reason", "Object", "Message"} + if showNs { + headers = append(namespaceHeader, headers...) + } + + return headers +} + +var fluxKinds = []string{"GitRepository", "HelmRepository", "OCIRepository", "Bucket", "HelmChart", "Kustomization", "HelmRelease", "Alert", "Provider", "ImageRepository", "ImagePolicy", "ImageUpdateAutomation"} + +func getEventRow(e corev1.Event, showNs bool) []string { + var row []string + if showNs { + row = []string{e.Namespace} + } + row = append(row, getLastSeen(e), e.Type, e.Reason, fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name), e.Message) + + return row +} + +// getObjectRef is used to get the metadata of a resource that the selector(in the format ) references. +// It returns an empty string if the resource doesn't reference any resource +// and a string with the format `/.` if it does. +func getObjectRef(ctx context.Context, kubeclient client.Client, selector string, ns string) ([]string, error) { + kind, name := utils.ParseObjectKindName(selector) + ref, err := getGroupVersionAndRef(kind, name, ns) + if err != nil { + return nil, fmt.Errorf("error getting groupversion: %w", err) + } + + // the resource has no source ref + if len(ref.field) == 0 { + return nil, nil + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: kind, + Version: ref.gv.Version, + Group: ref.gv.Group, + }) + objName := types.NamespacedName{ + Namespace: ns, + Name: name, + } + + err = kubeclient.Get(ctx, objName, obj) + if err != nil { + return nil, err + } + + var ok bool + refKind := ref.kind + if refKind == "" { + kindField := append(ref.field, "kind") + refKind, ok, err = unstructured.NestedString(obj.Object, kindField...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(kindField, "."), objName) + } + } + + nameField := append(ref.field, "name") + refName, ok, err := unstructured.NestedString(obj.Object, nameField...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(nameField, "."), objName) + } + + var allRefs []string + refNamespace := ns + if ref.crossNamespaced { + namespaceField := append(ref.field, "namespace") + namespace, ok, err := unstructured.NestedString(obj.Object, namespaceField...) + if err != nil { + return nil, err + } + if ok { + refNamespace = namespace + } + } + + allRefs = append(allRefs, fmt.Sprintf("%s/%s.%s", refKind, refName, refNamespace)) + for _, ref := range ref.otherRefs { + allRefs = append(allRefs, fmt.Sprintf("%s.%s", ref, refNamespace)) + } + + return allRefs, nil +} + +type refInfo struct { + gv schema.GroupVersion + kind string + crossNamespaced bool + otherRefs []string + field []string +} + +func getGroupVersionAndRef(kind, name, ns string) (refInfo, error) { + switch kind { + case kustomizev1.KustomizationKind: + return refInfo{ + gv: kustomizev1.GroupVersion, + crossNamespaced: true, + field: []string{"spec", "sourceRef"}, + }, nil + case helmv2beta1.HelmReleaseKind: + return refInfo{ + gv: helmv2beta1.GroupVersion, + crossNamespaced: true, + otherRefs: []string{fmt.Sprintf("HelmChart/%s-%s", ns, name)}, + field: []string{"spec", "chart", "spec", "sourceRef"}, + }, nil + case notificationv1.AlertKind: + return refInfo{ + gv: notificationv1.GroupVersion, + kind: notificationv1.ProviderKind, + crossNamespaced: false, + field: []string{"spec", "providerRef"}, + }, nil + case notificationv1.ReceiverKind, + notificationv1.ProviderKind: + return refInfo{ + gv: notificationv1.GroupVersion, + }, nil + case imagev1.ImagePolicyKind: + return refInfo{ + gv: imagev1.GroupVersion, + kind: imagev1.ImageRepositoryKind, + crossNamespaced: true, + field: []string{"spec", "imageRepositoryRef"}, + }, nil + case sourcev1.GitRepositoryKind, sourcev1.HelmChartKind, sourcev1.BucketKind, + sourcev1.HelmRepositoryKind, sourcev1.OCIRepositoryKind: + return refInfo{gv: sourcev1.GroupVersion}, nil + case autov1.ImageUpdateAutomationKind: + return refInfo{gv: autov1.GroupVersion}, nil + case imagev1.ImageRepositoryKind: + return refInfo{gv: imagev1.GroupVersion}, nil + default: + return refInfo{}, fmt.Errorf("'%s' is not a flux kind", kind) + } +} + +func filterEvent(e corev1.Event) bool { + if !utils.ContainsItemString(fluxKinds, e.InvolvedObject.Kind) { + return true + } + + if len(eventArgs.filterTypes) > 0 { + _, equal := utils.ContainsEqualFoldItemString(eventArgs.filterTypes, e.Type) + if !equal { + return true + } + } + + return false +} + +// The functions below are copied from: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/events/events.go#L347 + +// 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] +} + +// 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 +} + +func (list SortableEvents) Less(i, j int) bool { + return eventTime(list[i]).Before(eventTime(list[j])) +} + +func getLastSeen(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)) +} diff --git a/cmd/flux/events_test.go b/cmd/flux/events_test.go new file mode 100644 index 0000000000..12076c501b --- /dev/null +++ b/cmd/flux/events_test.go @@ -0,0 +1,411 @@ +/* +Copyright 2023 The Kubernetes Authors. +Copyright 2023 The Flux 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 main + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/fluxcd/flux2/internal/utils" + helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" + "github.com/fluxcd/pkg/ssa" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var objects = ` +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 5m0s + path: ./infrastructure/ + prune: true + sourceRef: + kind: GitRepository + name: flux-system +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: podinfo + namespace: default +spec: + interval: 5m0s + path: ./infrastructure/ + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: GitRepository +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 5m0s + ref: + branch: main + secretRef: + name: flux-system + timeout: 1m0s + url: ssh://git@github.com/example/repo +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: podinfo + namespace: default +spec: + chart: + spec: + chart: podinfo + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + version: '*' + interval: 5m0s +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + url: https://stefanprodan.github.io/podinfo +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmChart +metadata: + name: default-podinfo + namespace: flux-system +spec: + chart: podinfo + interval: 1m0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo-chart + version: '*' +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Alert +metadata: + name: webapp + namespace: flux-system +spec: + eventSeverity: info + eventSources: + - kind: GitRepository + name: '*' + providerRef: + name: slack +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta2 +kind: Provider +metadata: + name: slack + namespace: flux-system +spec: + address: https://hooks.slack.com/services/mock + type: slack +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImagePolicy +metadata: + name: podinfo + namespace: default +spec: + imageRepositoryRef: + name: acr-podinfo + namespace: flux-system + policy: + semver: + range: 5.0.x +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flux-system` + +func Test_getObjectRef(t *testing.T) { + g := NewWithT(t) + objs, err := ssa.ReadObjects(strings.NewReader(objects)) + g.Expect(err).To(Not(HaveOccurred())) + + builder := fake.NewClientBuilder().WithScheme(getScheme()) + for _, obj := range objs { + builder = builder.WithObjects(obj) + } + c := builder.Build() + + tests := []struct { + name string + selector string + namespace string + want []string + wantErr bool + }{ + { + name: "Source Ref for Kustomization", + selector: "Kustomization/flux-system", + namespace: "flux-system", + want: []string{"GitRepository/flux-system.flux-system"}, + }, + { + name: "Crossnamespace Source Ref for Kustomization", + selector: "Kustomization/podinfo", + namespace: "default", + want: []string{"GitRepository/flux-system.flux-system"}, + }, + { + name: "Source Ref for HelmRelease", + selector: "HelmRelease/podinfo", + namespace: "default", + want: []string{"HelmRepository/podinfo.flux-system", "HelmChart/default-podinfo.flux-system"}, + }, + { + name: "Source Ref for Alert", + selector: "Alert/webapp", + namespace: "flux-system", + want: []string{"Provider/slack.flux-system"}, + }, + { + name: "Source Ref for ImagePolicy", + selector: "ImagePolicy/podinfo", + namespace: "default", + want: []string{"ImageRepository/acr-podinfo.flux-system"}, + }, + { + name: "Empty Ref for Provider", + selector: "Provider/slack", + namespace: "flux-system", + want: nil, + }, + { + name: "Non flux resource", + selector: "Namespace/flux-system", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + got, err := getObjectRef(context.Background(), c, tt.selector, tt.namespace) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(got).To(Equal(tt.want)) + }) + } +} + +func Test_getRows(t *testing.T) { + g := NewWithT(t) + objs, err := ssa.ReadObjects(strings.NewReader(objects)) + g.Expect(err).To(Not(HaveOccurred())) + + builder := fake.NewClientBuilder().WithScheme(getScheme()) + for _, obj := range objs { + builder = builder.WithObjects(obj) + } + eventList := &corev1.EventList{} + for _, obj := range objs { + infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason") + warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason") + eventList.Items = append(eventList.Items, infoEvent, warningEvent) + } + builder = builder.WithLists(eventList) + builder.WithIndex(&corev1.Event{}, "involvedObject.kind/name", kindNameIndexer) + c := builder.Build() + + tests := []struct { + name string + selector string + refSelector string + namespace string + refNs string + expected [][]string + }{ + { + name: "events from all namespaces", + selector: "", + namespace: "", + expected: [][]string{ + {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"default", "", "error", "Error Reason", "ImagePolicy/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "ImagePolicy/podinfo", "Info Message"}, + {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Alert/webapp", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Alert/webapp", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "GitRepository/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "GitRepository/flux-system", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Kustomization/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Kustomization/flux-system", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Provider/slack", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Provider/slack", "Info Message"}, + }, + }, + { + name: "events from default namespaces", + selector: "", + namespace: "default", + expected: [][]string{ + {"", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"", "error", "Error Reason", "ImagePolicy/podinfo", "Error Message"}, + {"", "info", "Info Reason", "ImagePolicy/podinfo", "Info Message"}, + {"", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + }, + }, + { + name: "Kustomization with crossnamespaced GitRepository", + selector: "Kustomization/podinfo", + namespace: "default", + expected: [][]string{ + {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "GitRepository/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "GitRepository/flux-system", "Info Message"}, + }, + }, + { + name: "HelmRelease with crossnamespaced HelmRepository", + selector: "HelmRelease/podinfo", + namespace: "default", + expected: [][]string{ + {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + var refs []string + var refNs, refKind, refName string + if tt.selector != "" { + refs, err = getObjectRef(context.Background(), c, tt.selector, tt.namespace) + g.Expect(err).To(Not(HaveOccurred())) + } + + g.Expect(err).To(Not(HaveOccurred())) + + clientOpts := getTestListOpt(tt.namespace, tt.selector) + var refOpts [][]client.ListOption + for _, ref := range refs { + refKind, refName, refNs = utils.ParseObjectKindNameNamespace(ref) + refSelector := fmt.Sprintf("%s/%s", refKind, refName) + refOpts = append(refOpts, getTestListOpt(refNs, refSelector)) + } + + showNs := tt.namespace == "" || (refNs != "" && refNs != tt.namespace) + rows, err := getRows(context.Background(), c, clientOpts, refOpts, showNs) + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(rows).To(Equal(tt.expected)) + }) + } +} + +func getTestListOpt(namespace, selector string) []client.ListOption { + clientListOpts := []client.ListOption{client.Limit(cmdutil.DefaultChunkSize), client.InNamespace(namespace)} + if selector != "" { + sel := fields.OneTermEqualSelector("involvedObject.kind/name", selector) + clientListOpts = append(clientListOpts, client.MatchingFieldsSelector{Selector: sel}) + } + + return clientListOpts +} + +func getScheme() *runtime.Scheme { + newscheme := runtime.NewScheme() + corev1.AddToScheme(newscheme) + kustomizev1.AddToScheme(newscheme) + helmv2beta1.AddToScheme(newscheme) + notificationv1.AddToScheme(newscheme) + imagev1.AddToScheme(newscheme) + autov1.AddToScheme(newscheme) + sourcev1.AddToScheme(newscheme) + + return newscheme +} + +func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event { + return corev1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: obj.GetNamespace(), + // name of event needs to be unique so fak + Name: obj.GetNamespace() + obj.GetNamespace() + obj.GetObjectKind().GroupVersionKind().Kind + eventType, + }, + Reason: reason, + Message: msg, + Type: eventType, + InvolvedObject: corev1.ObjectReference{ + Kind: obj.GetObjectKind().GroupVersionKind().Kind, + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + }, + } +} + +func kindNameIndexer(obj client.Object) []string { + e, ok := obj.(*corev1.Event) + if !ok { + panic(fmt.Sprintf("Expected a Event, got %T", e)) + } + + return []string{fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name)} +} diff --git a/go.mod b/go.mod index 0766cc8758..2f058d1b78 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/fluxcd/image-reflector-controller/api v0.26.0 github.com/fluxcd/kustomize-controller/api v0.35.0 github.com/fluxcd/notification-controller/api v0.33.0 + github.com/fluxcd/pkg/apis/event v0.4.1 github.com/fluxcd/pkg/apis/meta v0.19.1 github.com/fluxcd/pkg/git v0.11.0 github.com/fluxcd/pkg/git/gogit v0.8.1 diff --git a/go.sum b/go.sum index 1e600ba272..b5156975df 100644 --- a/go.sum +++ b/go.sum @@ -211,6 +211,8 @@ github.com/fluxcd/notification-controller/api v0.33.0 h1:iVnIGDfkpVuzcILSGwi//Q3 github.com/fluxcd/notification-controller/api v0.33.0/go.mod h1:0IyWy0J6+z1TbijVQjFx1gWQDOzXaRfzV2NClfjHZPk= github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6pH4Q= github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8= +github.com/fluxcd/pkg/apis/event v0.4.1 h1:63wP8NM/uA4680F4Ft8q8/0rJivX90i7FmMkRvUI8Is= +github.com/fluxcd/pkg/apis/event v0.4.1/go.mod h1:LHT1ZsbMrcHwCHQCaFtQviQBZwhMOAbTUPK6+KgBkFo= github.com/fluxcd/pkg/apis/kustomize v0.8.1 h1:uRH9xVDJfSBGIiL6PIhkguHvf2Nme6uTWX+RX1iZznc= github.com/fluxcd/pkg/apis/kustomize v0.8.1/go.mod h1:TBem+2mHp6Ib7XD1fmzDkoUnBzx07wSzIYo6BVx3XAc= github.com/fluxcd/pkg/apis/meta v0.19.1 h1:fCI5CnTXpAqr67UlaI9q0H+OztMKB5kDTr6xV6vlAo0= From 5b8f673baa3cbe0c814f72434930708c63b9232e Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 9 Mar 2023 13:35:56 +0100 Subject: [PATCH 2/6] events: use constants for supported Flux kinds Signed-off-by: Hidde Beydals --- cmd/flux/events.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/flux/events.go b/cmd/flux/events.go index 6f9bba0592..da45b87e8d 100644 --- a/cmd/flux/events.go +++ b/cmd/flux/events.go @@ -39,7 +39,7 @@ import ( cmdutil "k8s.io/kubectl/pkg/cmd/util" "sigs.k8s.io/controller-runtime/pkg/client" - helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" @@ -268,7 +268,10 @@ func getHeaders(showNs bool) []string { return headers } -var fluxKinds = []string{"GitRepository", "HelmRepository", "OCIRepository", "Bucket", "HelmChart", "Kustomization", "HelmRelease", "Alert", "Provider", "ImageRepository", "ImagePolicy", "ImageUpdateAutomation"} +var fluxKinds = []string{sourcev1.GitRepositoryKind, sourcev1.HelmRepositoryKind, sourcev1.OCIRepositoryKind, + sourcev1.BucketKind, sourcev1.HelmChartKind, kustomizev1.KustomizationKind, helmv2.HelmReleaseKind, + notificationv1.AlertKind, notificationv1.ProviderKind, imagev1.ImageRepositoryKind, imagev1.ImagePolicyKind, + autov1.ImageUpdateAutomationKind} func getEventRow(e corev1.Event, showNs bool) []string { var row []string @@ -370,9 +373,9 @@ func getGroupVersionAndRef(kind, name, ns string) (refInfo, error) { crossNamespaced: true, field: []string{"spec", "sourceRef"}, }, nil - case helmv2beta1.HelmReleaseKind: + case helmv2.HelmReleaseKind: return refInfo{ - gv: helmv2beta1.GroupVersion, + gv: helmv2.GroupVersion, crossNamespaced: true, otherRefs: []string{fmt.Sprintf("HelmChart/%s-%s", ns, name)}, field: []string{"spec", "chart", "spec", "sourceRef"}, From 34220fd5148a30fa519cc9f5b68bf83d7c7497f6 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 9 Mar 2023 13:44:41 +0100 Subject: [PATCH 3/6] events: make `--for` case insensitive for kinds Signed-off-by: Hidde Beydals --- cmd/flux/events.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/cmd/flux/events.go b/cmd/flux/events.go index da45b87e8d..3521dc3711 100644 --- a/cmd/flux/events.go +++ b/cmd/flux/events.go @@ -155,7 +155,7 @@ func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []cli var rows [][]string for _, item := range el.Items { - if filterEvent(item) { + if ignoreEvent(item) { continue } rows = append(rows, getEventRow(item, showNs)) @@ -212,7 +212,7 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt if !ok { return nil } - if filterEvent(*event) { + if ignoreEvent(*event) { return nil } rows := getEventRow(*event, showNs) @@ -366,52 +366,52 @@ type refInfo struct { } func getGroupVersionAndRef(kind, name, ns string) (refInfo, error) { - switch kind { - case kustomizev1.KustomizationKind: + switch strings.ToLower(kind) { + case strings.ToLower(kustomizev1.KustomizationKind): return refInfo{ gv: kustomizev1.GroupVersion, crossNamespaced: true, field: []string{"spec", "sourceRef"}, }, nil - case helmv2.HelmReleaseKind: + case strings.ToLower(helmv2.HelmReleaseKind): return refInfo{ gv: helmv2.GroupVersion, crossNamespaced: true, - otherRefs: []string{fmt.Sprintf("HelmChart/%s-%s", ns, name)}, + otherRefs: []string{fmt.Sprintf("%s/%s-%s", sourcev1.HelmChartKind, ns, name)}, field: []string{"spec", "chart", "spec", "sourceRef"}, }, nil - case notificationv1.AlertKind: + case strings.ToLower(notificationv1.AlertKind): return refInfo{ gv: notificationv1.GroupVersion, kind: notificationv1.ProviderKind, crossNamespaced: false, field: []string{"spec", "providerRef"}, }, nil - case notificationv1.ReceiverKind, - notificationv1.ProviderKind: + case strings.ToLower(notificationv1.ReceiverKind), + strings.ToLower(notificationv1.ProviderKind): return refInfo{ gv: notificationv1.GroupVersion, }, nil - case imagev1.ImagePolicyKind: + case strings.ToLower(imagev1.ImagePolicyKind): return refInfo{ gv: imagev1.GroupVersion, kind: imagev1.ImageRepositoryKind, crossNamespaced: true, field: []string{"spec", "imageRepositoryRef"}, }, nil - case sourcev1.GitRepositoryKind, sourcev1.HelmChartKind, sourcev1.BucketKind, - sourcev1.HelmRepositoryKind, sourcev1.OCIRepositoryKind: + case strings.ToLower(sourcev1.GitRepositoryKind), strings.ToLower(sourcev1.HelmChartKind), strings.ToLower(sourcev1.BucketKind), + strings.ToLower(sourcev1.HelmRepositoryKind), strings.ToLower(sourcev1.OCIRepositoryKind): return refInfo{gv: sourcev1.GroupVersion}, nil - case autov1.ImageUpdateAutomationKind: + case strings.ToLower(autov1.ImageUpdateAutomationKind): return refInfo{gv: autov1.GroupVersion}, nil - case imagev1.ImageRepositoryKind: + case strings.ToLower(imagev1.ImageRepositoryKind): return refInfo{gv: imagev1.GroupVersion}, nil default: - return refInfo{}, fmt.Errorf("'%s' is not a flux kind", kind) + return refInfo{}, fmt.Errorf("'%s' is not a recognized Flux kind", kind) } } -func filterEvent(e corev1.Event) bool { +func ignoreEvent(e corev1.Event) bool { if !utils.ContainsItemString(fluxKinds, e.InvolvedObject.Kind) { return true } From c0916edc44e15f7bea4a6773b711bc5cd0886ed1 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 9 Mar 2023 13:54:18 +0100 Subject: [PATCH 4/6] events: prevent defer in loop Signed-off-by: Hidde Beydals --- cmd/flux/events.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/flux/events.go b/cmd/flux/events.go index 3521dc3711..1176bc0ce2 100644 --- a/cmd/flux/events.go +++ b/cmd/flux/events.go @@ -199,7 +199,6 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt if err != nil { return err } - defer eventWatch.Stop() firstIteration := true @@ -234,15 +233,19 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt if err != nil { return err } - defer refEventWatch.Stop() - go receiveEventChan(ctx, refEventWatch, handleEvent) + go func() { + err := receiveEventChan(ctx, refEventWatch, handleEvent) + if err != nil { + logger.Failuref("error watching events: %s", err.Error()) + } + }() } return receiveEventChan(ctx, eventWatch, handleEvent) - } func receiveEventChan(ctx context.Context, eventWatch watch.Interface, f func(e watch.Event) error) error { + defer eventWatch.Stop() for { select { case e, ok := <-eventWatch.ResultChan(): From 3f3d68a33aca414fc5d2439442b1597584f26824 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 9 Mar 2023 13:59:06 +0100 Subject: [PATCH 5/6] events: reword error messages Signed-off-by: Hidde Beydals --- cmd/flux/events.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/flux/events.go b/cmd/flux/events.go index 1176bc0ce2..d42368536a 100644 --- a/cmd/flux/events.go +++ b/cmd/flux/events.go @@ -127,9 +127,9 @@ func eventsCmdRun(cmd *cobra.Command, args []string) error { rows, err := getRows(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) if len(rows) == 0 { if eventArgs.allNamespaces { - logger.Failuref("No flux events found.") + logger.Failuref("No events found.") } else { - logger.Failuref("No flux events found in %s namespace.\n", *kubeconfigArgs.Namespace) + logger.Failuref("No events found in %s namespace.", *kubeconfigArgs.Namespace) } return nil From af153ea0cf4fd480c75accacc0c0dc7a2afdad0d Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 9 Mar 2023 14:18:16 +0100 Subject: [PATCH 6/6] events: avoid having to keep individal kind maps Signed-off-by: Hidde Beydals --- cmd/flux/events.go | 113 +++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/cmd/flux/events.go b/cmd/flux/events.go index d42368536a..ae25e0c31b 100644 --- a/cmd/flux/events.go +++ b/cmd/flux/events.go @@ -271,11 +271,6 @@ func getHeaders(showNs bool) []string { return headers } -var fluxKinds = []string{sourcev1.GitRepositoryKind, sourcev1.HelmRepositoryKind, sourcev1.OCIRepositoryKind, - sourcev1.BucketKind, sourcev1.HelmChartKind, kustomizev1.KustomizationKind, helmv2.HelmReleaseKind, - notificationv1.AlertKind, notificationv1.ProviderKind, imagev1.ImageRepositoryKind, imagev1.ImagePolicyKind, - autov1.ImageUpdateAutomationKind} - func getEventRow(e corev1.Event, showNs bool) []string { var row []string if showNs { @@ -291,7 +286,7 @@ func getEventRow(e corev1.Event, showNs bool) []string { // and a string with the format `/.` if it does. func getObjectRef(ctx context.Context, kubeclient client.Client, selector string, ns string) ([]string, error) { kind, name := utils.ParseObjectKindName(selector) - ref, err := getGroupVersionAndRef(kind, name, ns) + ref, err := fluxKindMap.getRefInfo(kind) if err != nil { return nil, fmt.Errorf("error getting groupversion: %w", err) } @@ -353,69 +348,77 @@ func getObjectRef(ctx context.Context, kubeclient client.Client, selector string } allRefs = append(allRefs, fmt.Sprintf("%s/%s.%s", refKind, refName, refNamespace)) - for _, ref := range ref.otherRefs { - allRefs = append(allRefs, fmt.Sprintf("%s.%s", ref, refNamespace)) + if ref.otherRefs != nil { + for _, otherRef := range ref.otherRefs(ns, name) { + allRefs = append(allRefs, fmt.Sprintf("%s.%s", otherRef, refNamespace)) + } } - return allRefs, nil } +type refMap map[string]refInfo + +func (r refMap) getRefInfo(kind string) (refInfo, error) { + for key, ref := range r { + if strings.EqualFold(key, kind) { + return ref, nil + } + } + return refInfo{}, fmt.Errorf("'%s' is not a recognized Flux kind", kind) +} + +func (r refMap) hasKind(kind string) bool { + _, err := r.getRefInfo(kind) + return err == nil +} + type refInfo struct { gv schema.GroupVersion kind string crossNamespaced bool - otherRefs []string + otherRefs func(namespace, name string) []string field []string } -func getGroupVersionAndRef(kind, name, ns string) (refInfo, error) { - switch strings.ToLower(kind) { - case strings.ToLower(kustomizev1.KustomizationKind): - return refInfo{ - gv: kustomizev1.GroupVersion, - crossNamespaced: true, - field: []string{"spec", "sourceRef"}, - }, nil - case strings.ToLower(helmv2.HelmReleaseKind): - return refInfo{ - gv: helmv2.GroupVersion, - crossNamespaced: true, - otherRefs: []string{fmt.Sprintf("%s/%s-%s", sourcev1.HelmChartKind, ns, name)}, - field: []string{"spec", "chart", "spec", "sourceRef"}, - }, nil - case strings.ToLower(notificationv1.AlertKind): - return refInfo{ - gv: notificationv1.GroupVersion, - kind: notificationv1.ProviderKind, - crossNamespaced: false, - field: []string{"spec", "providerRef"}, - }, nil - case strings.ToLower(notificationv1.ReceiverKind), - strings.ToLower(notificationv1.ProviderKind): - return refInfo{ - gv: notificationv1.GroupVersion, - }, nil - case strings.ToLower(imagev1.ImagePolicyKind): - return refInfo{ - gv: imagev1.GroupVersion, - kind: imagev1.ImageRepositoryKind, - crossNamespaced: true, - field: []string{"spec", "imageRepositoryRef"}, - }, nil - case strings.ToLower(sourcev1.GitRepositoryKind), strings.ToLower(sourcev1.HelmChartKind), strings.ToLower(sourcev1.BucketKind), - strings.ToLower(sourcev1.HelmRepositoryKind), strings.ToLower(sourcev1.OCIRepositoryKind): - return refInfo{gv: sourcev1.GroupVersion}, nil - case strings.ToLower(autov1.ImageUpdateAutomationKind): - return refInfo{gv: autov1.GroupVersion}, nil - case strings.ToLower(imagev1.ImageRepositoryKind): - return refInfo{gv: imagev1.GroupVersion}, nil - default: - return refInfo{}, fmt.Errorf("'%s' is not a recognized Flux kind", kind) - } +var fluxKindMap = refMap{ + kustomizev1.KustomizationKind: { + gv: kustomizev1.GroupVersion, + crossNamespaced: true, + field: []string{"spec", "sourceRef"}, + }, + helmv2.HelmReleaseKind: { + gv: helmv2.GroupVersion, + crossNamespaced: true, + otherRefs: func(namespace, name string) []string { + return []string{fmt.Sprintf("%s/%s-%s", sourcev1.HelmChartKind, namespace, name)} + }, + field: []string{"spec", "chart", "spec", "sourceRef"}, + }, + notificationv1.AlertKind: { + gv: notificationv1.GroupVersion, + kind: notificationv1.ProviderKind, + crossNamespaced: false, + field: []string{"spec", "providerRef"}, + }, + notificationv1.ReceiverKind: {gv: notificationv1.GroupVersion}, + notificationv1.ProviderKind: {gv: notificationv1.GroupVersion}, + imagev1.ImagePolicyKind: { + gv: imagev1.GroupVersion, + kind: imagev1.ImageRepositoryKind, + crossNamespaced: true, + field: []string{"spec", "imageRepositoryRef"}, + }, + sourcev1.GitRepositoryKind: {gv: sourcev1.GroupVersion}, + sourcev1.OCIRepositoryKind: {gv: sourcev1.GroupVersion}, + sourcev1.BucketKind: {gv: sourcev1.GroupVersion}, + sourcev1.HelmRepositoryKind: {gv: sourcev1.GroupVersion}, + sourcev1.HelmChartKind: {gv: sourcev1.GroupVersion}, + autov1.ImageUpdateAutomationKind: {gv: autov1.GroupVersion}, + imagev1.ImageRepositoryKind: {gv: imagev1.GroupVersion}, } func ignoreEvent(e corev1.Event) bool { - if !utils.ContainsItemString(fluxKinds, e.InvolvedObject.Kind) { + if !fluxKindMap.hasKind(e.InvolvedObject.Kind) { return true }