From 1680f71ac6bb0facbbce4c1bdffe2c175aba6466 Mon Sep 17 00:00:00 2001 From: shawnh2 Date: Thu, 1 Feb 2024 23:19:06 +0800 Subject: [PATCH] add support for egctl x status --- internal/cmd/egctl/experimental.go | 5 +- internal/cmd/egctl/stats.go | 2 +- internal/cmd/egctl/status.go | 313 +++++++++++++++++++++++++++ internal/cmd/egctl/translate.go | 2 +- internal/cmd/egctl/translate_test.go | 2 +- internal/cmd/egctl/utils.go | 51 +++-- 6 files changed, 355 insertions(+), 20 deletions(-) create mode 100644 internal/cmd/egctl/status.go diff --git a/internal/cmd/egctl/experimental.go b/internal/cmd/egctl/experimental.go index 7fb744fb8de4..e22dc3e6ca60 100644 --- a/internal/cmd/egctl/experimental.go +++ b/internal/cmd/egctl/experimental.go @@ -22,8 +22,9 @@ func newExperimentalCommand() *cobra.Command { `, } - experimentalCommand.AddCommand(NewTranslateCommand()) - experimentalCommand.AddCommand(statsCommand()) + experimentalCommand.AddCommand(newTranslateCommand()) + experimentalCommand.AddCommand(newStatsCommand()) + experimentalCommand.AddCommand(newStatusCommand()) return experimentalCommand } diff --git a/internal/cmd/egctl/stats.go b/internal/cmd/egctl/stats.go index a16338721781..bf9554be86b0 100644 --- a/internal/cmd/egctl/stats.go +++ b/internal/cmd/egctl/stats.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra" ) -func statsCommand() *cobra.Command { +func newStatsCommand() *cobra.Command { c := &cobra.Command{ Use: "stats", Long: "Retrieve statistics from envoy proxy.", diff --git a/internal/cmd/egctl/status.go b/internal/cmd/egctl/status.go new file mode 100644 index 000000000000..fb7d87c5e5fd --- /dev/null +++ b/internal/cmd/egctl/status.go @@ -0,0 +1,313 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package egctl + +import ( + "context" + "fmt" + "os" + "reflect" + "strconv" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func newStatusCommand() *cobra.Command { + var ( + quiet, verbose, allNamespaces bool + resourceType, namespace string + ) + + statusCommand := &cobra.Command{ + Use: "status", + Short: "Show the summary of the status of resources in Envoy Gateway", + Example: ` # Show the status of gatewayclass resources under default namespace. + egctl x status gatewayclass + + # Show the status of gateway resources with less information under default namespace. + egctl x status gateway -q + + # Show the status of gateway resources with details under default namespace. + egctl x status gateway -v + + # Show the status of httproutes resources with details under a specific namespace. + egctl x status httproutes -v -n foobar + + # Show the status of httproutes resources under all namespaces. + egctl x status httproutes -A + `, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + table := tabwriter.NewWriter(os.Stdout, 10, 0, 3, ' ', 0) + + if len(args) == 1 { + resourceType = args[0] + } else if len(args) > 1 { + return fmt.Errorf("unknown args: %s", strings.Join(args[1:], ",")) + } else { + return fmt.Errorf("invalid args: must sepecific a resources type") + } + + return runStatus(ctx, table, resourceType, namespace, quiet, verbose, allNamespaces) + }, + } + + statusCommand.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Show the status of resources only") + statusCommand.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show the status of resources with details") + statusCommand.PersistentFlags().BoolVarP(&allNamespaces, "all-namespaces", "A", false, "Get resources from all namespaces") + statusCommand.PersistentFlags().StringVarP(&namespace, "namespace", "n", "default", "Specific a namespace to get resources") + + return statusCommand +} + +func runStatus(ctx context.Context, table *tabwriter.Writer, resourceType, namespace string, quiet, verbose, allNamespaces bool) error { + k8sClient, err := newK8sClient() + if err != nil { + return err + } + + var resourcesList client.ObjectList + + if allNamespaces { + namespace = "" + } + + switch strings.ToLower(resourceType) { + case "gc", "gcs", "gatewayclass", "gatewayclasses": + gc := gwv1.GatewayClassList{} + if err = k8sClient.List(ctx, &gc, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &gc + + case "gtw", "gtws", "gateway", "gateways": + gtw := gwv1.GatewayList{} + if err = k8sClient.List(ctx, >w, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = >w + + case "httproute", "httproutes": + httproute := gwv1.HTTPRouteList{} + if err = k8sClient.List(ctx, &httproute, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &httproute + + case "grpcroute", "grpcroutes": + grpcroute := gwv1a2.GRPCRouteList{} + if err = k8sClient.List(ctx, &grpcroute, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &grpcroute + + case "tcproute", "tcproutes": + tcproute := gwv1a2.TCPRouteList{} + if err = k8sClient.List(ctx, &tcproute, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &tcproute + + case "udproute", "udproutes": + udproute := gwv1a2.UDPRouteList{} + if err = k8sClient.List(ctx, &udproute, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &udproute + + case "tlsroute", "tlsroutes": + tlsroute := gwv1a2.TLSRouteList{} + if err = k8sClient.List(ctx, &tlsroute, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &tlsroute + + case "btlspolicy", "btlspolicies", "backendtlspolicy", "backendtlspolicies": + btlspolicy := gwv1a2.BackendTLSPolicyList{} + if err = k8sClient.List(ctx, &btlspolicy, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &btlspolicy + + case "btp", "btps", "backendtrafficpolicy", "backendtrafficpolicies": + btp := egv1a1.BackendTrafficPolicyList{} + if err = k8sClient.List(ctx, &btp, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &btp + + case "ctp", "ctps", "clienttrafficpolicy", "clienttrafficpolicies": + ctp := egv1a1.ClientTrafficPolicyList{} + if err = k8sClient.List(ctx, &ctp, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &ctp + + case "epp", "epps", "enovypatchpolicy", "envoypatchpolicies": + epp := egv1a1.EnvoyPatchPolicyList{} + if err = k8sClient.List(ctx, &epp, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &epp + + case "sp", "sps", "securitypolicy", "secruitypolicies": + sp := egv1a1.SecurityPolicyList{} + if err = k8sClient.List(ctx, &sp, client.InNamespace(namespace)); err != nil { + return err + } + resourcesList = &sp + + default: + return fmt.Errorf("unknown resource type: %s", resourceType) + } + + namespaced, err := k8sClient.IsObjectNamespaced(resourcesList) + if err != nil { + return err + } + + needNamespaceHeader := allNamespaces && namespaced + writeStatusHeaders(table, verbose, needNamespaceHeader) + + if err = writeStatusBodies(table, resourcesList, resourceType, quiet, verbose, needNamespaceHeader); err != nil { + return err + } + + return table.Flush() +} + +func writeStatusHeaders(table *tabwriter.Writer, verbose, needNamespace bool) { + headers := []string{"NAME", "TYPE", "STATUS", "REASON"} + + if needNamespace { + headers = append([]string{"NAMESPACE"}, headers...) + } + if verbose { + headers = append(headers, []string{"MESSAGE", "OBSERVED GENERATION", "LAST TRANSITION TIME"}...) + } + + fmt.Fprintln(table, strings.Join(headers, "\t")) +} + +func writeStatusBodies(table *tabwriter.Writer, resourcesList client.ObjectList, resourceType string, quiet, verbose, needNamespace bool) error { + v := reflect.ValueOf(resourcesList).Elem() + + itemsField := v.FieldByName("Items") + if !itemsField.IsValid() { + return fmt.Errorf("failed to load `.Items` field from %s", resourceType) + } + + for i := 0; i < itemsField.Len(); i++ { + item := itemsField.Index(i) + + var name, namespace string + nameField := item.FieldByName("Name") + if !nameField.IsValid() { + return fmt.Errorf("failed to find `.Items[i].Name` field from %s", resourceType) + } + name = nameField.String() + + if needNamespace { + namespaceField := item.FieldByName("Namespace") + if !namespaceField.IsValid() { + return fmt.Errorf("failed to find `.Items[i].Namespace` field from %s", resourceType) + } + namespace = namespaceField.String() + } + + statusField := item.FieldByName("Status") + if !statusField.IsValid() { + return fmt.Errorf("failed to find `.Items[i].Status` field from %s", resourceType) + } + + if strings.Contains(resourceType, "route") { + // Scrape conditions from `Resource.Status.Parents[i].Conditions` field + parentsField := statusField.FieldByName("Parents") + if !parentsField.IsValid() { + return fmt.Errorf("failed to find `.Items[i].Status.Parents` field from %s", resourceType) + } + + for j := 0; j < parentsField.Len(); j++ { + parentItem := parentsField.Index(j) + if err := findAndWriteConditions(table, parentItem, resourceType, name, namespace, quiet, verbose, needNamespace); err != nil { + return err + } + } + } else if strings.Contains(resourceType, "btls") || strings.Contains(resourceType, "backendtls") { + // Scrape conditions from `Resource.Status.Ancestors[i].Conditions` field + ancestorsField := statusField.FieldByName("Ancestors") + if !ancestorsField.IsValid() { + return fmt.Errorf("failed to find `.Items[i].Status.Ancestors` field from %s", resourceType) + } + + for j := 0; j < ancestorsField.Len(); j++ { + ancestorItem := ancestorsField.Index(j) + if err := findAndWriteConditions(table, ancestorItem, resourceType, name, namespace, quiet, verbose, needNamespace); err != nil { + return err + } + } + } else { + // Scrape conditions from `Resource.Status.Conditions` field + return findAndWriteConditions(table, statusField, resourceType, name, namespace, quiet, verbose, needNamespace) + } + } + + return nil +} + +func findAndWriteConditions(table *tabwriter.Writer, parent reflect.Value, resourceType, name, namespace string, quiet, verbose, needNamespace bool) error { + conditionsField := parent.FieldByName("Conditions") + if !conditionsField.IsValid() { + return fmt.Errorf("failed to find `Conditions` field for %s", resourceType) + } + + conditions := conditionsField.Interface().([]metav1.Condition) + writeConditions(table, conditions, name, namespace, quiet, verbose, needNamespace) + + return nil +} + +func writeConditions(table *tabwriter.Writer, conditions []metav1.Condition, name, namespace string, quiet, verbose, needNamespace bool) { + // Sort in descending order by time of each condition. + for i := len(conditions) - 1; i >= 0; i-- { + if i < len(conditions)-1 { + name, namespace = "", "" + } + + writeCondition(table, conditions[i], name, namespace, verbose, needNamespace) + + if quiet { + break + } + } +} + +// writeCondition writes condition data corresponding to its header. +func writeCondition(table *tabwriter.Writer, condition metav1.Condition, name, namespace string, verbose, needNamespace bool) { + row := []string{name, condition.Type, string(condition.Status), condition.Reason} + + if needNamespace { + row = append([]string{namespace}, row...) + } + if verbose { + row = append(row, []string{ + condition.Message, + strconv.FormatInt(condition.ObservedGeneration, 10), + condition.LastTransitionTime.String(), + }...) + } + + fmt.Fprintln(table, strings.Join(row, "\t")) +} diff --git a/internal/cmd/egctl/translate.go b/internal/cmd/egctl/translate.go index 47ab944f4243..7dd5bcce4c48 100644 --- a/internal/cmd/egctl/translate.go +++ b/internal/cmd/egctl/translate.go @@ -52,7 +52,7 @@ type TranslationResult struct { Xds map[string]interface{} `json:"xds,omitempty"` } -func NewTranslateCommand() *cobra.Command { +func newTranslateCommand() *cobra.Command { var ( inFile, inType, output, resourceType string addMissingResources bool diff --git a/internal/cmd/egctl/translate_test.go b/internal/cmd/egctl/translate_test.go index 02bd04c9f34a..11ac62c95a09 100644 --- a/internal/cmd/egctl/translate_test.go +++ b/internal/cmd/egctl/translate_test.go @@ -284,7 +284,7 @@ func TestTranslate(t *testing.T) { t.Run(tc.name+"|"+tc.resourceType, func(t *testing.T) { b := bytes.NewBufferString("") - root := NewTranslateCommand() + root := newTranslateCommand() root.SetOut(b) root.SetErr(b) args := []string{ diff --git a/internal/cmd/egctl/utils.go b/internal/cmd/egctl/utils.go index f1029247ae35..4b8a4510eeb6 100644 --- a/internal/cmd/egctl/utils.go +++ b/internal/cmd/egctl/utils.go @@ -10,6 +10,13 @@ import ( adminv3 "github.com/envoyproxy/go-control-plane/envoy/admin/v3" "google.golang.org/protobuf/reflect/protoreflect" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" + gwv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) type envoyConfigType string @@ -26,34 +33,34 @@ var ( func findXDSResourceFromConfigDump(resourceType envoyConfigType, globalConfigs *adminv3.ConfigDump) (protoreflect.ProtoMessage, error) { switch resourceType { case BootstrapEnvoyConfigType: - for _, config := range globalConfigs.Configs { - if config.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump" { - return config, nil + for _, cfg := range globalConfigs.Configs { + if cfg.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.BootstrapConfigDump" { + return cfg, nil } } case EndpointEnvoyConfigType: - for _, config := range globalConfigs.Configs { - if config.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump" { - return config, nil + for _, cfg := range globalConfigs.Configs { + if cfg.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.EndpointsConfigDump" { + return cfg, nil } } case ClusterEnvoyConfigType: - for _, config := range globalConfigs.Configs { - if config.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.ClustersConfigDump" { - return config, nil + for _, cfg := range globalConfigs.Configs { + if cfg.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.ClustersConfigDump" { + return cfg, nil } } case ListenerEnvoyConfigType: - for _, config := range globalConfigs.Configs { - if config.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.ListenersConfigDump" { - return config, nil + for _, cfg := range globalConfigs.Configs { + if cfg.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.ListenersConfigDump" { + return cfg, nil } } case RouteEnvoyConfigType: - for _, config := range globalConfigs.Configs { - if config.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.RoutesConfigDump" { - return config, nil + for _, cfg := range globalConfigs.Configs { + if cfg.GetTypeUrl() == "type.googleapis.com/envoy.admin.v3.RoutesConfigDump" { + return cfg, nil } } case AllEnvoyConfigType: @@ -64,3 +71,17 @@ func findXDSResourceFromConfigDump(resourceType envoyConfigType, globalConfigs * return nil, fmt.Errorf("unknown resourceType %s", resourceType) } + +func newK8sClient() (client.Client, error) { + cli, err := client.New(config.GetConfigOrDie(), client.Options{}) + if err != nil { + return nil, fmt.Errorf("failed to initialize Kubernetes client: %v", err) + } + + gwv1.AddToScheme(cli.Scheme()) + gwv1b1.AddToScheme(cli.Scheme()) + gwv1a2.AddToScheme(cli.Scheme()) + egv1a1.AddToScheme(cli.Scheme()) + + return cli, nil +}