diff --git a/cmd/pkg.go b/cmd/pkg.go index 809efdf71a41673b4a9e93853c0697eaa0bd7747..e0ba3bc644c6f9ab650b6c42bf7ea7177fa9a993 100644 --- a/cmd/pkg.go +++ b/cmd/pkg.go @@ -23,7 +23,7 @@ import ( "github.com/ksonnet/ksonnet/metadata" "github.com/ksonnet/ksonnet/metadata/parts" - str "github.com/ksonnet/ksonnet/strings" + "github.com/ksonnet/ksonnet/pkg/util/table" "github.com/spf13/cobra" ) @@ -248,13 +248,9 @@ var pkgListCmd = &cobra.Command{ return err } - headerRows := [][]string{ - []string{registryHeader, nameHeader, installedHeader}, - []string{ - strings.Repeat("=", len(registryHeader)), - strings.Repeat("=", len(nameHeader)), - strings.Repeat("=", len(installedHeader))}, - } + t := table.New(os.Stdout) + t.SetHeader([]string{registryHeader, nameHeader, installedHeader}) + rows := make([][]string, 0) for name := range app.Registries() { reg, _, err := manager.GetRegistry(name) @@ -263,12 +259,15 @@ var pkgListCmd = &cobra.Command{ } for libName := range reg.Libraries { + var row []string _, isInstalled := app.Libraries()[libName] if isInstalled { - rows = append(rows, []string{name, libName, installed}) + row = []string{name, libName, installed} } else { - rows = append(rows, []string{name, libName}) + row = []string{name, libName} } + + rows = append(rows, row) } } @@ -279,13 +278,9 @@ var pkgListCmd = &cobra.Command{ return nameI < nameJ }) - rows = append(headerRows, rows...) + t.AppendBulk(rows) + t.Render() - formatted, err := str.PadRows(rows) - if err != nil { - return err - } - fmt.Print(formatted) return nil }, Long: ` diff --git a/cmd/registry.go b/cmd/registry.go index 32ec425f38430e806c079d2a43e30fd60bac507e..1ee4dcf2bcf68bc3c72dea15dea90f35ba6e2c9b 100644 --- a/cmd/registry.go +++ b/cmd/registry.go @@ -23,7 +23,7 @@ import ( "github.com/ksonnet/ksonnet/metadata" "github.com/ksonnet/ksonnet/pkg/kubecfg" - str "github.com/ksonnet/ksonnet/strings" + "github.com/ksonnet/ksonnet/pkg/util/table" "github.com/spf13/cobra" ) @@ -103,23 +103,15 @@ var registryListCmd = &cobra.Command{ return err } - rows := [][]string{ - []string{nameHeader, protocolHeader, uriHeader}, - []string{ - strings.Repeat("=", len(nameHeader)), - strings.Repeat("=", len(protocolHeader)), - strings.Repeat("=", len(uriHeader)), - }, - } + t := table.New(os.Stdout) + t.SetHeader([]string{nameHeader, protocolHeader, uriHeader}) + for name, regRef := range app.Registries() { - rows = append(rows, []string{name, regRef.Protocol, regRef.URI}) + t.Append([]string{name, regRef.Protocol, regRef.URI}) } - formatted, err := str.PadRows(rows) - if err != nil { - return err - } - fmt.Print(formatted) + t.Render() + return nil }, Long: ` diff --git a/pkg/kubecfg/component.go b/pkg/kubecfg/component.go index 88a275507699ec30961fa224c55f0ecb9cfa6125..80619174540463479d4fd1796dcf64dd16dd7eac 100644 --- a/pkg/kubecfg/component.go +++ b/pkg/kubecfg/component.go @@ -16,12 +16,10 @@ package kubecfg import ( - "fmt" "io" "sort" - "strings" - str "github.com/ksonnet/ksonnet/strings" + "github.com/ksonnet/ksonnet/pkg/util/table" ) const ( @@ -49,8 +47,7 @@ func (c *ComponentListCmd) Run(out io.Writer) error { return err } - _, err = printComponents(out, components) - return err + return printComponents(out, components) } // ComponentRmCmd stores the information necessary to remove a component from @@ -74,21 +71,14 @@ func (c *ComponentRmCmd) Run() error { return manager.DeleteComponent(c.component) } -func printComponents(out io.Writer, components []string) (string, error) { - rows := [][]string{ - []string{componentNameHeader}, - []string{strings.Repeat("=", len(componentNameHeader))}, - } +func printComponents(out io.Writer, components []string) error { + t := table.New(out) + t.SetHeader([]string{componentNameHeader}) sort.Strings(components) for _, component := range components { - rows = append(rows, []string{component}) + t.Append([]string{component}) } - formatted, err := str.PadRows(rows) - if err != nil { - return "", err - } - _, err = fmt.Fprint(out, formatted) - return formatted, err + return t.Render() } diff --git a/pkg/kubecfg/component_test.go b/pkg/kubecfg/component_test.go index e2cc526a422d11b6873617c5a78cfc763f366d86..0303ca4b6e2b5e5ba4b1378f37b873dbf00694ee 100644 --- a/pkg/kubecfg/component_test.go +++ b/pkg/kubecfg/component_test.go @@ -16,18 +16,20 @@ package kubecfg import ( - "os" + "bytes" "testing" "github.com/stretchr/testify/require" ) func TestPrintComponents(t *testing.T) { - for _, tc := range []struct { + cases := []struct { + name string components []string expected string }{ { + name: "print", components: []string{"a", "b"}, expected: `COMPONENT ========= @@ -35,8 +37,8 @@ a b `, }, - // Check that components are displayed in alphabetical order { + name: "Check that components are displayed in alphabetical order", components: []string{"b", "a"}, expected: `COMPONENT ========= @@ -44,18 +46,21 @@ a b `, }, - // Check empty components scenario { + name: "Check empty components scenario", components: []string{}, expected: `COMPONENT ========= `, }, - } { - out, err := printComponents(os.Stdout, tc.components) - if err != nil { - t.Fatalf(err.Error()) - } - require.EqualValues(t, tc.expected, out) + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + err := printComponents(&buf, tc.components) + require.NoError(t, err) + require.EqualValues(t, tc.expected, buf.String()) + }) } } diff --git a/pkg/kubecfg/env.go b/pkg/kubecfg/env.go index f9bc84a0926cf9428828f019756bd052367d2008..3dbe700677b45543ff02ca4abf15781ba6d2a53d 100644 --- a/pkg/kubecfg/env.go +++ b/pkg/kubecfg/env.go @@ -16,14 +16,12 @@ package kubecfg import ( - "fmt" "io" "sort" - "strings" "github.com/ksonnet/ksonnet/env" "github.com/ksonnet/ksonnet/metadata" - str "github.com/ksonnet/ksonnet/strings" + "github.com/ksonnet/ksonnet/pkg/util/table" ) type EnvAddCmd struct { @@ -69,6 +67,7 @@ func NewEnvListCmd(manager metadata.Manager) (*EnvListCmd, error) { return &EnvListCmd{manager: manager}, nil } +// Run builds a list of environments. func (c *EnvListCmd) Run(out io.Writer) error { const ( nameHeader = "NAME" @@ -90,30 +89,19 @@ func (c *EnvListCmd) Run(out io.Writer) error { // Sort environments by ascending alphabetical name sort.Slice(envs, func(i, j int) bool { return envs[i].Name < envs[j].Name }) - rows := [][]string{ - []string{nameHeader, k8sVersionHeader, namespaceHeader, serverHeader}, - []string{ - strings.Repeat("=", len(nameHeader)), - strings.Repeat("=", len(k8sVersionHeader)), - strings.Repeat("=", len(namespaceHeader)), - strings.Repeat("=", len(serverHeader))}, - } + t := table.New(out) + t.SetHeader([]string{nameHeader, k8sVersionHeader, namespaceHeader, serverHeader}) for _, env := range envs { - rows = append(rows, []string{ + t.Append([]string{ env.Name, env.KubernetesVersion, env.Destination.Namespace(), env.Destination.Server()}) } - formattedEnvsList, err := str.PadRows(rows) - if err != nil { - return err - } - - _, err = fmt.Fprint(out, formattedEnvsList) - return err + t.Render() + return nil } // ================================================================== diff --git a/pkg/kubecfg/param.go b/pkg/kubecfg/param.go index 2483ca03b336003bc6bde26b33ca4aaa8fc9c0bd..d723653e28c405630fef6436019bdeabb25ddc52 100644 --- a/pkg/kubecfg/param.go +++ b/pkg/kubecfg/param.go @@ -22,11 +22,11 @@ import ( "reflect" "sort" "strconv" - "strings" "github.com/fatih/color" "github.com/ksonnet/ksonnet/component" param "github.com/ksonnet/ksonnet/metadata/params" + "github.com/ksonnet/ksonnet/pkg/util/table" str "github.com/ksonnet/ksonnet/strings" "github.com/pkg/errors" "github.com/spf13/afero" @@ -162,56 +162,42 @@ func (c *ParamListCmd) Run(out io.Writer) error { } p := params[c.component] - return outputParamsFor(c.component, p, out) + outputParamsFor(c.component, p, out) + return nil } - return outputParams(params, out) + outputParams(params, out) + return nil } -func outputParamsFor(component string, params param.Params, out io.Writer) error { +func outputParamsFor(component string, params param.Params, out io.Writer) { keys := sortedParams(params) - rows := [][]string{ - []string{paramNameHeader, paramValueHeader}, - []string{strings.Repeat("=", len(paramNameHeader)), strings.Repeat("=", len(paramValueHeader))}, - } + t := table.New(out) + t.SetHeader([]string{paramNameHeader, paramValueHeader}) for _, k := range keys { - rows = append(rows, []string{k, params[k]}) + t.Append([]string{k, params[k]}) } - formatted, err := str.PadRows(rows) - if err != nil { - return err - } - _, err = fmt.Fprint(out, formatted) - return err + t.Render() } -func outputParams(params map[string]param.Params, out io.Writer) error { +func outputParams(params map[string]param.Params, out io.Writer) { keys := sortedKeys(params) - rows := [][]string{ - []string{paramComponentHeader, paramNameHeader, paramValueHeader}, - []string{ - strings.Repeat("=", len(paramComponentHeader)), - strings.Repeat("=", len(paramNameHeader)), - strings.Repeat("=", len(paramValueHeader))}, - } + t := table.New(out) + t.SetHeader([]string{paramComponentHeader, paramNameHeader, paramValueHeader}) + for _, k := range keys { // sort params to display alphabetically ps := sortedParams(params[k]) for _, p := range ps { - rows = append(rows, []string{k, p, params[k][p]}) + t.Append([]string{k, p, params[k][p]}) } } - formatted, err := str.PadRows(rows) - if err != nil { - return err - } - _, err = fmt.Fprint(out, formatted) - return err + t.Render() } // ---------------------------------------------------------------------------- diff --git a/pkg/util/table/table.go b/pkg/util/table/table.go new file mode 100644 index 0000000000000000000000000000000000000000..85351000cb387d84a7e000884ca622cf1ab7b359 --- /dev/null +++ b/pkg/util/table/table.go @@ -0,0 +1,123 @@ +// Copyright 2018 The ksonnet 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 table + +import ( + "fmt" + "io" + "strings" + + "github.com/pkg/errors" +) + +const ( + // sepChar is the character used to separate the header from the content in a table. + sepChar = "=" +) + +// Table creates an output table. +type Table struct { + w io.Writer + + header []string + rows [][]string +} + +// New creates an instance of table. +func New(w io.Writer) *Table { + return &Table{ + w: w, + } +} + +// SetHeader sets the header for the table. +func (t *Table) SetHeader(columns []string) { + t.header = columns +} + +// Append appends a row to the table. +func (t *Table) Append(row []string) { + t.rows = append(t.rows, row) +} + +// AppendBulk appends multiple rows to the table. +func (t *Table) AppendBulk(rows [][]string) { + t.rows = append(t.rows, rows...) +} + +// Render writes the output to the table's writer. +func (t *Table) Render() error { + var output [][]string + + if len(t.header) > 0 { + headerRow := make([]string, len(t.header), len(t.header)) + sepRow := make([]string, len(t.header), len(t.header)) + + for i := range t.header { + sepLen := len(t.header[i]) + headerRow[i] = strings.ToUpper(t.header[i]) + sepRow[i] = strings.Repeat(sepChar, sepLen) + } + + output = append(output, headerRow, sepRow) + } + + output = append(output, t.rows...) + + counts := colLens(output) + + // print rows + for _, row := range output { + var parts []string + for i, col := range row { + val := col + if i < len(row)-1 { + format := fmt.Sprintf("%%-%ds", counts[i]) + val = fmt.Sprintf(format, col) + } + parts = append(parts, val) + + } + _, err := fmt.Fprintf(t.w, "%s\n", strings.Join(parts, " ")) + if err != nil { + return errors.Wrap(err, "render table") + } + } + + return nil +} + +func colLens(rows [][]string) []int { + // count the number of columns + colCount := 0 + for _, row := range rows { + if l := len(row); l > colCount { + colCount = l + } + } + + // get the max len for each column + counts := make([]int, colCount, colCount) + for _, row := range rows { + for i := range row { + if l := len(row[i]); l > counts[i] { + counts[i] = l + } + } + } + + return counts +} diff --git a/pkg/util/table/table_test.go b/pkg/util/table/table_test.go new file mode 100644 index 0000000000000000000000000000000000000000..375b0b4ed528becc102674f512b761620ee54b5a --- /dev/null +++ b/pkg/util/table/table_test.go @@ -0,0 +1,62 @@ +// Copyright 2018 The ksonnet 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 table + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTable(t *testing.T) { + var buf bytes.Buffer + table := New(&buf) + + table.SetHeader([]string{"name", "version", "Namespace", "SERVER"}) + table.Append([]string{"default", "v1.7.0", "default", "http://default"}) + table.AppendBulk([][]string{ + {"dev", "v1.8.0", "dev", "http://dev"}, + {"east/prod", "v1.8.0", "east/prod", "http://east-prod"}, + }) + + table.Render() + + b, err := ioutil.ReadFile("testdata/table.txt") + require.NoError(t, err) + + assert.Equal(t, string(b), buf.String()) +} + +func TestTable_no_header(t *testing.T) { + var buf bytes.Buffer + table := New(&buf) + + table.Append([]string{"default", "v1.7.0", "default", "http://default"}) + table.AppendBulk([][]string{ + {"dev", "v1.8.0", "dev", "http://dev"}, + {"east/prod", "v1.8.0", "east/prod", "http://east-prod"}, + }) + + table.Render() + + b, err := ioutil.ReadFile("testdata/table_no_header.txt") + require.NoError(t, err) + + assert.Equal(t, string(b), buf.String()) +} diff --git a/pkg/util/table/testdata/table.txt b/pkg/util/table/testdata/table.txt new file mode 100644 index 0000000000000000000000000000000000000000..e064b929f6a320c1c82ac0ec817d19e183000ad5 --- /dev/null +++ b/pkg/util/table/testdata/table.txt @@ -0,0 +1,5 @@ +NAME VERSION NAMESPACE SERVER +==== ======= ========= ====== +default v1.7.0 default http://default +dev v1.8.0 dev http://dev +east/prod v1.8.0 east/prod http://east-prod diff --git a/pkg/util/table/testdata/table_no_header.txt b/pkg/util/table/testdata/table_no_header.txt new file mode 100644 index 0000000000000000000000000000000000000000..b4e6ac3128a52e92062d6eb37aaa4d3eb07bb87d --- /dev/null +++ b/pkg/util/table/testdata/table_no_header.txt @@ -0,0 +1,3 @@ +default v1.7.0 default http://default +dev v1.8.0 dev http://dev +east/prod v1.8.0 east/prod http://east-prod diff --git a/prototype/specification.go b/prototype/specification.go index 7df294c8a5c8c4759b4122c5cf16824f1416a81c..234ea8e203c277ca06c5d5d75c20cd48ea26ee05 100644 --- a/prototype/specification.go +++ b/prototype/specification.go @@ -23,9 +23,8 @@ import ( "strings" "github.com/blang/semver" - str "github.com/ksonnet/ksonnet/strings" + "github.com/ksonnet/ksonnet/pkg/util/table" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" ) // @@ -180,20 +179,17 @@ func (ss SpecificationSchemas) String() string { sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name }) - rows := [][]string{ - []string{nameHeader, descriptionHeader}, - []string{strings.Repeat("=", len(nameHeader)), strings.Repeat("=", len(descriptionHeader))}, - } + var buf bytes.Buffer + t := table.New(&buf) + t.SetHeader([]string{nameHeader, descriptionHeader}) + for _, proto := range ss { - rows = append(rows, []string{proto.Name, proto.Template.ShortDescription}) + t.Append([]string{proto.Name, proto.Template.ShortDescription}) } - formatted, err := str.PadRows(rows) - if err != nil { - log.Errorf("Failed to print spec rows:\n%v", err) - } + t.Render() - return formatted + return buf.String() } // RequiredParams retrieves all parameters that are required by a prototype.