From c33eaa0609ba4288773265cfc8bf4780886063f6 Mon Sep 17 00:00:00 2001 From: Somefive Date: Wed, 13 Apr 2022 17:46:55 +0800 Subject: [PATCH] Fix: enhance vela status tree print (#3639) Signed-off-by: Somefive --- go.mod | 2 +- pkg/resourcetracker/tree.go | 75 +++++++++++++++++++++++++++++--- pkg/resourcetracker/tree_test.go | 48 ++++++++++++++++++++ references/cli/status.go | 12 +++-- 4 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 pkg/resourcetracker/tree_test.go diff --git a/go.mod b/go.mod index 1a622b46d..f8248cf58 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( go.uber.org/zap v1.18.1 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/tools v0.1.6 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df @@ -259,7 +260,6 @@ require ( golang.org/x/mod v0.4.2 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect diff --git a/pkg/resourcetracker/tree.go b/pkg/resourcetracker/tree.go index ab73e7430..37d067096 100644 --- a/pkg/resourcetracker/tree.go +++ b/pkg/resourcetracker/tree.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "io" + "math" "net/http" "sort" "strings" @@ -45,11 +46,15 @@ import ( ) // ResourceDetailRetriever retriever to get details for resource -type ResourceDetailRetriever func(*resourceRow) error +type ResourceDetailRetriever func(*resourceRow, string) error // ResourceTreePrintOptions print options for resource tree type ResourceTreePrintOptions struct { DetailRetriever ResourceDetailRetriever + // MaxWidth if set, the detail part will auto wrap + MaxWidth *int + // Format for details + Format string } const ( @@ -171,9 +176,64 @@ func (options *ResourceTreePrintOptions) fillResourceRows(rows []*resourceRow, c colsWidth[1] += 4 } -func (options *ResourceTreePrintOptions) writeResourceTree(writer io.Writer, rows []*resourceRow, colsWidth []int) { - applyTimeWidth := 20 +const ( + applyTimeWidth = 20 + detailMinWidth = 20 +) +func (options *ResourceTreePrintOptions) _getWidthForDetails(colsWidth []int) int { + detailWidth := 0 + if options.MaxWidth == nil { + return math.MaxInt + } + detailWidth = *options.MaxWidth - applyTimeWidth + for _, width := range colsWidth { + detailWidth -= width + } + // if the space for details exceeds the max allowed width, give up wrapping lines + if detailWidth < detailMinWidth { + detailWidth = math.MaxInt + } + return detailWidth +} + +func (options *ResourceTreePrintOptions) _wrapDetails(detail string, width int) (lines []string) { + for _, row := range strings.Split(detail, "\n") { + var sb strings.Builder + row = strings.ReplaceAll(row, "\t", " ") + for _, token := range strings.Split(row, " ") { + if sb.Len()+len(token)+2 <= width { + if sb.Len() > 0 { + sb.WriteString(" ") + } + sb.WriteString(token) + } else { + if sb.Len() > 0 { + lines = append(lines, sb.String()) + sb.Reset() + } + offset := 0 + for { + if offset+width > len(token) { + break + } + lines = append(lines, token[offset:offset+width]) + offset += width + } + sb.WriteString(token[offset:]) + } + } + if sb.Len() > 0 { + lines = append(lines, sb.String()) + } + } + if len(lines) == 0 { + lines = []string{""} + } + return lines +} + +func (options *ResourceTreePrintOptions) writeResourceTree(writer io.Writer, rows []*resourceRow, colsWidth []int) { writePaddedString := func(sb *strings.Builder, head string, tail string, width int) { sb.WriteString(head) for c := strutil.StringWidth(head) + strutil.StringWidth(tail); c < width; c++ { @@ -195,14 +255,15 @@ func (options *ResourceTreePrintOptions) writeResourceTree(writer io.Writer, row connectorColorizer := color.WhiteString outdatedColorizer := color.WhiteString + detailWidth := options._getWidthForDetails(colsWidth) for _, row := range rows { if options.DetailRetriever != nil && row.status != resourceRowStatusNotDeployed { - if err := options.DetailRetriever(row); err != nil { + if err := options.DetailRetriever(row, options.Format); err != nil { row.details = "Error: " + err.Error() } } - for lineIdx, line := range strings.Split(row.details, "\n") { + for lineIdx, line := range options._wrapDetails(row.details, detailWidth) { var sb strings.Builder rscName, rscStatus, applyTime := row.resourceName, row.status, row.applyTime if row.status != resourceRowStatusUpdated { @@ -275,7 +336,7 @@ func (rt tableRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) } // RetrieveKubeCtlGetMessageGenerator get details like kubectl get -func RetrieveKubeCtlGetMessageGenerator(cfg *rest.Config, format string) (ResourceDetailRetriever, error) { +func RetrieveKubeCtlGetMessageGenerator(cfg *rest.Config) (ResourceDetailRetriever, error) { cfg.Wrap(func(rt http.RoundTripper) http.RoundTripper { return tableRoundTripper{rt: rt} }) @@ -283,7 +344,7 @@ func RetrieveKubeCtlGetMessageGenerator(cfg *rest.Config, format string) (Resour if err != nil { return nil, err } - return func(row *resourceRow) error { + return func(row *resourceRow, format string) error { mr := row.mr un := &unstructured.Unstructured{} un.SetAPIVersion(mr.APIVersion) diff --git a/pkg/resourcetracker/tree_test.go b/pkg/resourcetracker/tree_test.go new file mode 100644 index 000000000..ed9cc94ff --- /dev/null +++ b/pkg/resourcetracker/tree_test.go @@ -0,0 +1,48 @@ +/* +Copyright 2021 The KubeVela 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 resourcetracker + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/utils/pointer" +) + +func TestResourceTreePrintOption_getWidthForDetails(t *testing.T) { + r := require.New(t) + options := &ResourceTreePrintOptions{} + r.Equal(math.MaxInt, options._getWidthForDetails(nil)) + options.MaxWidth = pointer.Int(50 + applyTimeWidth) + r.Equal(30, options._getWidthForDetails([]int{10, 10})) + r.Equal(math.MaxInt, options._getWidthForDetails([]int{20, 20})) +} + +func TestResourceTreePrintOptions_wrapDetails(t *testing.T) { + r := require.New(t) + options := &ResourceTreePrintOptions{} + detail := "test-key: test-val\ttest-data: test-val\ntest-next-line: text-next-value test-long-key: test long long long long value test-append: test-append-val" + r.Equal( + []string{ + "test-key: test-val test-data: test-val", + "test-next-line: text-next-value", + "test-long-key: test long long long long ", + "value test-append: test-append-val", + }, + options._wrapDetails(detail, 40)) +} diff --git a/references/cli/status.go b/references/cli/status.go index f0419b124..51f5c0a38 100644 --- a/references/cli/status.go +++ b/references/cli/status.go @@ -27,6 +27,8 @@ import ( "github.com/olekukonko/tablewriter" "github.com/pkg/errors" "github.com/spf13/cobra" + "golang.org/x/term" + "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/client" commontypes "github.com/oam-dev/kubevela/apis/core.oam.dev/common" @@ -396,11 +398,15 @@ func printApplicationTree(c common.Args, cmd *cobra.Command, appName string, app if err == nil { placements, _ = policy.GetPlacementsFromTopologyPolicies(context.Background(), cli, app, af.Policies, true) } - options := resourcetracker.ResourceTreePrintOptions{} - printDetails, _ := cmd.Flags().GetBool("detail") format, _ := cmd.Flags().GetString("detail-format") + var maxWidth *int + if w, _, err := term.GetSize(0); err == nil && w > 0 { + maxWidth = pointer.Int(w) + } + options := resourcetracker.ResourceTreePrintOptions{MaxWidth: maxWidth, Format: format} + printDetails, _ := cmd.Flags().GetBool("detail") if printDetails { - msgRetriever, err := resourcetracker.RetrieveKubeCtlGetMessageGenerator(config, format) + msgRetriever, err := resourcetracker.RetrieveKubeCtlGetMessageGenerator(config) if err != nil { return err }