Skip to content

Commit bdee3be

Browse files
committed
kepctl output: extract output logic to pkg/output
Repo seems to be all about CRUD for KEPs. In that context, formatting output seem like an unrelated concern. Use an interface/factory pattern to build and use an Output to display results. Next step: make Query return results, make callers use Output.
1 parent 5fb5d54 commit bdee3be

File tree

4 files changed

+248
-206
lines changed

4 files changed

+248
-206
lines changed

pkg/kepctl/commands/query.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/pkg/errors"
2323
"github.com/spf13/cobra"
2424

25+
"k8s.io/enhancements/pkg/output"
2526
"k8s.io/enhancements/pkg/repo"
2627
)
2728

@@ -96,10 +97,8 @@ func addQuery(topLevel *cobra.Command) {
9697
cmd.PersistentFlags().StringVar(
9798
&qo.Output,
9899
"output",
99-
repo.DefaultOutputOpt,
100-
fmt.Sprintf(
101-
"Output format. Can be %v", repo.SupportedOutputOpts,
102-
),
100+
output.DefaultFormat,
101+
fmt.Sprintf("Output format. Can be %v", output.ValidFormats()),
103102
)
104103

105104
topLevel.AddCommand(cmd)

pkg/output/output.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package output
18+
19+
import (
20+
"encoding/csv"
21+
"encoding/json"
22+
"fmt"
23+
"io"
24+
"strings"
25+
26+
"github.com/olekukonko/tablewriter"
27+
"gopkg.in/yaml.v3"
28+
29+
"k8s.io/enhancements/api"
30+
)
31+
32+
const (
33+
DefaultFormat = "table"
34+
)
35+
36+
// ValidFormats returns the list of valid formats for NewOutput
37+
func ValidFormats() []string {
38+
return []string{"csv", "json", "table", "yaml"}
39+
}
40+
41+
// Output is capable of printing... well pretty much just KEPs, but if there
42+
// is interest in printing other types in api, this could be the place
43+
type Output interface {
44+
PrintProposals([]*api.Proposal)
45+
}
46+
47+
// output holds out/err streams for Output implementations
48+
type output struct {
49+
Out io.Writer
50+
Err io.Writer
51+
}
52+
53+
// columnOutput holds PrintConfigs in addition to out/err streams for column
54+
// oriented Output implementations (e.g. CSVOutput)
55+
type columnOutput struct {
56+
output
57+
Configs []PrintConfig
58+
}
59+
60+
// NewOutput returns an Output of the given format that will write to the given
61+
// out and err Writers, or return an err if the format isn't one of ValidFormats()
62+
func NewOutput(format string, out, err io.Writer) (Output, error) {
63+
switch format {
64+
case "json":
65+
return &JSONOutput{Out: out, Err: err}, nil
66+
case "yaml":
67+
return &YAMLOutput{Out: out, Err: err}, nil
68+
case "csv":
69+
return &CSVOutput{
70+
output: output{
71+
Out: out,
72+
Err: err,
73+
},
74+
Configs: DefaultPrintConfigs(
75+
"Title",
76+
"Authors",
77+
"SIG",
78+
"Stage",
79+
"Status",
80+
"LastUpdated",
81+
"Link",
82+
),
83+
}, nil
84+
case "table":
85+
return &TableOutput{
86+
output: output{
87+
Out: out,
88+
Err: err,
89+
},
90+
Configs: DefaultPrintConfigs(
91+
"LastUpdated",
92+
"Stage",
93+
"Status",
94+
"SIG",
95+
"Authors",
96+
"Title",
97+
"Link",
98+
),
99+
}, nil
100+
}
101+
return nil, fmt.Errorf("unsupported output format: %s. Valid values: %s", format, ValidFormats())
102+
}
103+
104+
type YAMLOutput output
105+
106+
// PrintProposals prints KEPs array in YAML format
107+
func (o *YAMLOutput) PrintProposals(proposals []*api.Proposal) {
108+
data, err := yaml.Marshal(proposals)
109+
if err != nil {
110+
fmt.Fprintf(o.Err, "error printing KEPs as YAML: %s", err)
111+
return
112+
}
113+
fmt.Fprintln(o.Out, string(data))
114+
}
115+
116+
type JSONOutput output
117+
118+
// PrintProposals prints KEPs array in JSON format
119+
func (o *JSONOutput) PrintProposals(proposals []*api.Proposal) {
120+
data, err := json.Marshal(proposals)
121+
if err != nil {
122+
fmt.Fprintf(o.Err, "error printing KEPs as JSON: %s", err)
123+
return
124+
}
125+
126+
fmt.Fprintln(o.Out, string(data))
127+
}
128+
129+
type TableOutput columnOutput
130+
131+
// PrintProposals prints KEPs array in table format
132+
func (o *TableOutput) PrintProposals(proposals []*api.Proposal) {
133+
if len(o.Configs) == 0 {
134+
return
135+
}
136+
137+
table := tablewriter.NewWriter(o.Out)
138+
139+
headers := make([]string, 0, len(o.Configs))
140+
for _, c := range o.Configs {
141+
headers = append(headers, c.Title())
142+
}
143+
144+
table.SetHeader(headers)
145+
table.SetAlignment(tablewriter.ALIGN_LEFT)
146+
147+
for _, k := range proposals {
148+
var s []string
149+
for _, c := range o.Configs {
150+
s = append(s, c.Value(k))
151+
}
152+
table.Append(s)
153+
}
154+
155+
table.Render()
156+
}
157+
158+
type CSVOutput columnOutput
159+
160+
// PrintProposals prins KEPs array in CSV format
161+
func (o *CSVOutput) PrintProposals(proposals []*api.Proposal) {
162+
w := csv.NewWriter(o.Out)
163+
defer w.Flush()
164+
165+
headers := make([]string, 0, len(o.Configs))
166+
for _, c := range o.Configs {
167+
headers = append(headers, c.Title())
168+
}
169+
if err := w.Write(headers); err != nil {
170+
fmt.Fprintf(o.Err, "error printing keps as CSV: %s", err)
171+
}
172+
173+
for _, p := range proposals {
174+
var row []string
175+
for _, c := range o.Configs {
176+
row = append(row, c.Value(p))
177+
}
178+
if err := w.Write(row); err != nil {
179+
fmt.Fprintf(o.Err, "error printing keps as CSV: %s", err)
180+
}
181+
}
182+
}
183+
184+
// PrintConfig defines how a given Proposal field should be formatted for
185+
// columnar output (e.g. TableOutput, CSVOutput)
186+
type PrintConfig interface {
187+
Title() string
188+
Value(*api.Proposal) string
189+
}
190+
191+
type printConfig struct {
192+
title string
193+
valueFunc func(*api.Proposal) string
194+
}
195+
196+
func (p *printConfig) Title() string { return p.title }
197+
func (p *printConfig) Value(k *api.Proposal) string {
198+
return p.valueFunc(k)
199+
}
200+
201+
func DefaultPrintConfigs(names ...string) []PrintConfig {
202+
// TODO: Refactor out anonymous funcs
203+
defaultConfig := map[string]printConfig{
204+
"Authors": {"Authors", func(k *api.Proposal) string { return strings.Join(k.Authors, ", ") }},
205+
"LastUpdated": {"Updated", func(k *api.Proposal) string { return k.LastUpdated }},
206+
"SIG": {"SIG", func(k *api.Proposal) string {
207+
if strings.HasPrefix(k.OwningSIG, "sig-") {
208+
return k.OwningSIG[4:]
209+
}
210+
211+
return k.OwningSIG
212+
}},
213+
"Stage": {"Stage", func(k *api.Proposal) string { return k.Stage }},
214+
"Status": {"Status", func(k *api.Proposal) string { return k.Status }},
215+
"Title": {"Title", func(k *api.Proposal) string {
216+
if k.PRNumber == "" {
217+
return k.Title
218+
}
219+
220+
return "PR#" + k.PRNumber + " - " + k.Title
221+
}},
222+
"Link": {"Link", func(k *api.Proposal) string {
223+
if k.PRNumber == "" {
224+
return "https://git.k8s.io/enhancements/keps/" + k.OwningSIG + "/" + k.Name
225+
}
226+
227+
return "https://github.com/kubernetes/enhancements/pull/" + k.PRNumber
228+
}},
229+
}
230+
configs := make([]PrintConfig, 0, 10)
231+
for _, n := range names {
232+
// copy to allow it to be tweaked by the caller
233+
c := defaultConfig[n]
234+
configs = append(configs, &c)
235+
}
236+
return configs
237+
}

0 commit comments

Comments
 (0)