mirror of https://github.com/knative/client.git
Refactor describe: printing levels and shared kn code (#430)
* Refactor describe: printing levels and shared kn code * Test this refactor more * Comments
This commit is contained in:
parent
b871112e1f
commit
aafdb9a238
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright © 2019 The Knative 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 commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
"knative.dev/client/pkg/printers"
|
||||
"knative.dev/pkg/apis"
|
||||
)
|
||||
|
||||
// Max length When to truncate long strings (when not "all" mode switched on)
|
||||
const TruncateAt = 100
|
||||
|
||||
func WriteMetadata(dw printers.PrefixWriter, m *metav1.ObjectMeta, printDetails bool) {
|
||||
dw.WriteAttribute("Name", m.Name)
|
||||
dw.WriteAttribute("Namespace", m.Namespace)
|
||||
WriteMapDesc(dw, m.Labels, l("Labels"), "", printDetails)
|
||||
WriteMapDesc(dw, m.Annotations, l("Annotations"), "", printDetails)
|
||||
dw.WriteAttribute("Age", Age(m.CreationTimestamp.Time))
|
||||
}
|
||||
|
||||
var boringDomains = map[string]bool{
|
||||
"serving.knative.dev": true,
|
||||
"client.knative.dev": true,
|
||||
"kubectl.kubernetes.io": true,
|
||||
}
|
||||
|
||||
func keyIsBoring(k string) bool {
|
||||
parts := strings.Split(k, "/")
|
||||
return len(parts) > 1 && boringDomains[parts[0]]
|
||||
}
|
||||
|
||||
// Write a map either compact in a single line (possibly truncated) or, if printDetails is set,
|
||||
// over multiple line, one line per key-value pair. The output is sorted by keys.
|
||||
func WriteMapDesc(dw printers.PrefixWriter, m map[string]string, label string, labelPrefix string, details bool) {
|
||||
if len(m) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range m {
|
||||
if details || !keyIsBoring(k) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if details {
|
||||
l := labelPrefix + label
|
||||
|
||||
for _, key := range keys {
|
||||
dw.WriteColsLn(l, key+"="+m[key])
|
||||
l = labelPrefix
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
dw.WriteColsLn(label, joinAndTruncate(keys, m, TruncateAt-len(label)-2))
|
||||
}
|
||||
|
||||
func Age(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return duration.ShortHumanDuration(time.Now().Sub(t))
|
||||
}
|
||||
|
||||
func formatConditionType(condition apis.Condition) string {
|
||||
return string(condition.Type)
|
||||
}
|
||||
|
||||
// Status in ASCII format
|
||||
func formatStatus(c apis.Condition) string {
|
||||
switch c.Status {
|
||||
case corev1.ConditionTrue:
|
||||
return "++"
|
||||
case corev1.ConditionFalse:
|
||||
switch c.Severity {
|
||||
case apis.ConditionSeverityError:
|
||||
return "!!"
|
||||
case apis.ConditionSeverityWarning:
|
||||
return " W"
|
||||
case apis.ConditionSeverityInfo:
|
||||
return " I"
|
||||
default:
|
||||
return " !"
|
||||
}
|
||||
default:
|
||||
return "??"
|
||||
}
|
||||
}
|
||||
|
||||
// Used for conditions table to do own formatting for the table,
|
||||
// as the tabbed writer doesn't work nicely with colors
|
||||
func getMaxTypeLen(conditions []apis.Condition) int {
|
||||
max := 0
|
||||
for _, condition := range conditions {
|
||||
if len(condition.Type) > max {
|
||||
max = len(condition.Type)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// Sort conditions: Ready first, followed by error, then Warning, then Info
|
||||
func sortConditions(conditions []apis.Condition) []apis.Condition {
|
||||
// Don't change the orig slice
|
||||
ret := make([]apis.Condition, len(conditions))
|
||||
for i, c := range conditions {
|
||||
ret[i] = c
|
||||
}
|
||||
sort.SliceStable(ret, func(i, j int) bool {
|
||||
ic := &ret[i]
|
||||
jc := &ret[j]
|
||||
// Ready first
|
||||
if ic.Type == apis.ConditionReady {
|
||||
return jc.Type != apis.ConditionReady
|
||||
}
|
||||
if jc.Type == apis.ConditionReady {
|
||||
return false
|
||||
}
|
||||
// Among conditions of the same Severity, sort by Type
|
||||
if ic.Severity == jc.Severity {
|
||||
return ic.Type < jc.Type
|
||||
}
|
||||
// Error < Warning < Info
|
||||
switch ic.Severity {
|
||||
case apis.ConditionSeverityError:
|
||||
return true
|
||||
case apis.ConditionSeverityWarning:
|
||||
return jc.Severity == apis.ConditionSeverityInfo
|
||||
case apis.ConditionSeverityInfo:
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// Print out a table with conditions.
|
||||
func WriteConditions(dw printers.PrefixWriter, conditions []apis.Condition, printMessage bool) {
|
||||
section := dw.WriteAttribute("Conditions", "")
|
||||
conditions = sortConditions(conditions)
|
||||
maxLen := getMaxTypeLen(conditions)
|
||||
formatHeader := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n"
|
||||
formatRow := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n"
|
||||
section.Writef(formatHeader, "OK", "TYPE", "AGE", "REASON")
|
||||
for _, condition := range conditions {
|
||||
ok := formatStatus(condition)
|
||||
reason := condition.Reason
|
||||
if printMessage && reason != "" {
|
||||
reason = fmt.Sprintf("%s (%s)", reason, condition.Message)
|
||||
}
|
||||
section.Writef(formatRow, ok, formatConditionType(condition), Age(condition.LastTransitionTime.Inner.Time), reason)
|
||||
}
|
||||
}
|
||||
|
||||
// Format label (extracted so that color could be added more easily to all labels)
|
||||
func l(label string) string {
|
||||
return label + ":"
|
||||
}
|
||||
|
||||
// Join to key=value pair, comma separated, and truncate if longer than a limit
|
||||
func joinAndTruncate(sortedKeys []string, m map[string]string, width int) string {
|
||||
ret := ""
|
||||
for _, key := range sortedKeys {
|
||||
ret += fmt.Sprintf("%s=%s, ", key, m[key])
|
||||
if len(ret) > width {
|
||||
break
|
||||
}
|
||||
}
|
||||
// cut of two latest chars
|
||||
ret = strings.TrimRight(ret, ", ")
|
||||
if len(ret) <= width {
|
||||
return ret
|
||||
}
|
||||
return string(ret[:width-4]) + " ..."
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright © 2019 The Knative 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 commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
"knative.dev/client/pkg/printers"
|
||||
"knative.dev/pkg/apis"
|
||||
)
|
||||
|
||||
var testMap = map[string]string{
|
||||
"a": "b",
|
||||
"c": "d",
|
||||
"foo": "bar",
|
||||
"serving.knative.dev/funky": "chicken",
|
||||
}
|
||||
|
||||
func TestWriteMapDesc(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
dw := printers.NewBarePrefixWriter(buf)
|
||||
WriteMapDesc(dw, testMap, "eggs", "", false)
|
||||
assert.Equal(t, buf.String(), "eggs\ta=b, c=d, foo=bar\n")
|
||||
}
|
||||
|
||||
func TestWriteMapDescDetailed(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
dw := printers.NewBarePrefixWriter(buf)
|
||||
WriteMapDesc(dw, testMap, "eggs", "", true)
|
||||
assert.Equal(t, buf.String(), "eggs\ta=b\n\tc=d\n\tfoo=bar\n\tserving.knative.dev/funky=chicken\n")
|
||||
}
|
||||
|
||||
func TestWriteMapTruncated(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
dw := printers.NewBarePrefixWriter(buf)
|
||||
|
||||
items := map[string]string{}
|
||||
for i := 0; i < 1000; i++ {
|
||||
items[strconv.Itoa(i)] = strconv.Itoa(i + 1)
|
||||
}
|
||||
WriteMapDesc(dw, items, "eggs", "", false)
|
||||
assert.Assert(t, len(strings.TrimSpace(buf.String())) <= TruncateAt)
|
||||
}
|
||||
|
||||
var someConditions = []apis.Condition{
|
||||
{Type: apis.ConditionReady, Status: "True"},
|
||||
{Type: "Aaa", Status: "True"},
|
||||
{Type: "Zzz", Status: "False"},
|
||||
{Type: "Bbb", Status: "False", Severity: apis.ConditionSeverityWarning, Reason: "Bad"},
|
||||
{Type: "Ccc", Status: "False", Severity: apis.ConditionSeverityInfo, Reason: "Eh."},
|
||||
}
|
||||
var permutations = [][]int{
|
||||
{0, 1, 2, 3, 4},
|
||||
{4, 3, 2, 1, 0},
|
||||
{2, 1, 4, 3, 0},
|
||||
{2, 1, 0, 3, 4},
|
||||
}
|
||||
|
||||
func TestSortConditions(t *testing.T) {
|
||||
for _, p := range permutations {
|
||||
permuted := make([]apis.Condition, len(someConditions))
|
||||
for i, j := range p {
|
||||
permuted[i] = someConditions[j]
|
||||
}
|
||||
sorted := sortConditions(permuted)
|
||||
assert.DeepEqual(t, sorted, someConditions)
|
||||
}
|
||||
}
|
||||
|
||||
var spaces = regexp.MustCompile("\\s*")
|
||||
|
||||
func normalizeSpace(s string) string {
|
||||
return spaces.ReplaceAllLiteralString(s, " ")
|
||||
}
|
||||
|
||||
func TestWriteConditions(t *testing.T) {
|
||||
for _, p := range permutations {
|
||||
permuted := make([]apis.Condition, len(someConditions))
|
||||
for i, j := range p {
|
||||
permuted[i] = someConditions[j]
|
||||
}
|
||||
buf := &bytes.Buffer{}
|
||||
dw := printers.NewBarePrefixWriter(buf)
|
||||
WriteConditions(dw, permuted, false)
|
||||
assert.Equal(t, normalizeSpace(buf.String()), normalizeSpace(`Conditions:
|
||||
OK TYPE AGE REASON
|
||||
++ Ready
|
||||
++ Aaa
|
||||
!! Zzz
|
||||
W Bbb Bad
|
||||
I Ccc Eh.`))
|
||||
}
|
||||
}
|
||||
|
|
@ -33,15 +33,11 @@ import (
|
|||
client_serving "knative.dev/client/pkg/serving"
|
||||
serving_kn_v1alpha1 "knative.dev/client/pkg/serving/v1alpha1"
|
||||
|
||||
"knative.dev/pkg/apis"
|
||||
duckv1 "knative.dev/pkg/apis/duck/v1"
|
||||
"knative.dev/serving/pkg/apis/serving/v1alpha1"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/util/duration"
|
||||
|
||||
"knative.dev/client/pkg/kn/commands"
|
||||
)
|
||||
|
||||
|
|
@ -52,9 +48,6 @@ import (
|
|||
// Whether to print extended information
|
||||
var printDetails bool
|
||||
|
||||
// Max length When to truncate long strings (when not "all" mode switched on)
|
||||
const truncateAt = 100
|
||||
|
||||
// Matching image digest
|
||||
var imageDigestRegexp = regexp.MustCompile(`(?i)sha256:([0-9a-f]{64})`)
|
||||
|
||||
|
|
@ -190,7 +183,7 @@ func describe(w io.Writer, service *v1alpha1.Service, revisions []*revisionDesc,
|
|||
}
|
||||
|
||||
// Condition info
|
||||
writeConditions(dw, service)
|
||||
commands.WriteConditions(dw, service.Status.Conditions, printDetails)
|
||||
if err := dw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -200,76 +193,56 @@ func describe(w io.Writer, service *v1alpha1.Service, revisions []*revisionDesc,
|
|||
|
||||
// Write out main service information. Use colors for major items.
|
||||
func writeService(dw printers.PrefixWriter, service *v1alpha1.Service) {
|
||||
dw.WriteColsLn(printers.Level0, l("Name"), service.Name)
|
||||
dw.WriteColsLn(printers.Level0, l("Namespace"), service.Namespace)
|
||||
dw.WriteColsLn(printers.Level0, l("URL"), extractURL(service))
|
||||
commands.WriteMetadata(dw, &service.ObjectMeta, printDetails)
|
||||
dw.WriteAttribute("URL", extractURL(service))
|
||||
if service.Status.Address != nil {
|
||||
url := service.Status.Address.GetURL()
|
||||
dw.WriteColsLn(printers.Level0, l("Address"), url.String())
|
||||
dw.WriteAttribute("Address", url.String())
|
||||
}
|
||||
writeMapDesc(dw, printers.Level0, service.Labels, l("Labels"), "")
|
||||
writeMapDesc(dw, printers.Level0, service.Annotations, l("Annotations"), "")
|
||||
dw.WriteColsLn(printers.Level0, l("Age"), age(service.CreationTimestamp.Time))
|
||||
}
|
||||
|
||||
// Write out revisions associated with this service. By default only active
|
||||
// target revisions are printed, but with --all also inactive revisions
|
||||
// created by this services are shown
|
||||
func writeRevisions(dw printers.PrefixWriter, revisions []*revisionDesc, printDetails bool) {
|
||||
dw.WriteColsLn(printers.Level0, l("Revisions"))
|
||||
revSection := dw.WriteAttribute("Revisions", "")
|
||||
dw.Flush()
|
||||
for _, revisionDesc := range revisions {
|
||||
dw.WriteColsLn(printers.Level1, formatBullet(revisionDesc.percent, revisionDesc.ready), revisionHeader(revisionDesc))
|
||||
section := revSection.WriteColsLn(formatBullet(revisionDesc.percent, revisionDesc.ready), revisionHeader(revisionDesc))
|
||||
if revisionDesc.ready == v1.ConditionFalse {
|
||||
dw.WriteColsLn(printers.Level1, "", l("Error"), revisionDesc.reason)
|
||||
section.WriteAttribute("Error", revisionDesc.reason)
|
||||
}
|
||||
dw.WriteColsLn(printers.Level1, "", l("Image"), getImageDesc(revisionDesc))
|
||||
section.WriteAttribute("Image", getImageDesc(revisionDesc))
|
||||
if printDetails {
|
||||
if revisionDesc.port != nil {
|
||||
dw.WriteColsLn(printers.Level1, "", l("Port"), strconv.FormatInt(int64(*revisionDesc.port), 10))
|
||||
section.WriteAttribute("Port", strconv.FormatInt(int64(*revisionDesc.port), 10))
|
||||
}
|
||||
writeSliceDesc(dw, printers.Level1, revisionDesc.env, l("Env"), "\t")
|
||||
writeSliceDesc(section, revisionDesc.env, l("Env"), "")
|
||||
|
||||
// Scale spec if given
|
||||
if revisionDesc.maxScale != nil || revisionDesc.minScale != nil {
|
||||
dw.WriteColsLn(printers.Level1, "", l("Scale"), formatScale(revisionDesc.minScale, revisionDesc.maxScale))
|
||||
section.WriteAttribute("Scale", formatScale(revisionDesc.minScale, revisionDesc.maxScale))
|
||||
}
|
||||
|
||||
// Concurrency specs if given
|
||||
if revisionDesc.concurrencyLimit != nil || revisionDesc.concurrencyTarget != nil {
|
||||
writeConcurrencyOptions(dw, revisionDesc)
|
||||
writeConcurrencyOptions(section, revisionDesc)
|
||||
}
|
||||
|
||||
// Resources if given
|
||||
writeResources(dw, "Memory", revisionDesc.requestsMemory, revisionDesc.limitsMemory)
|
||||
writeResources(dw, "CPU", revisionDesc.requestsCPU, revisionDesc.limitsCPU)
|
||||
writeResources(section, "Memory", revisionDesc.requestsMemory, revisionDesc.limitsMemory)
|
||||
writeResources(section, "CPU", revisionDesc.requestsCPU, revisionDesc.limitsCPU)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print out a table with conditions. Use green for 'ok', and red for 'nok' if color is enabled
|
||||
func writeConditions(dw printers.PrefixWriter, service *v1alpha1.Service) {
|
||||
dw.WriteColsLn(printers.Level0, l("Conditions"))
|
||||
maxLen := getMaxTypeLen(service.Status.Conditions)
|
||||
formatHeader := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n"
|
||||
formatRow := "%-2s %-" + strconv.Itoa(maxLen) + "s %6s %-s\n"
|
||||
dw.Write(printers.Level1, formatHeader, "OK", "TYPE", "AGE", "REASON")
|
||||
for _, condition := range service.Status.Conditions {
|
||||
ok := formatStatus(condition.Status)
|
||||
reason := condition.Reason
|
||||
if printDetails && reason != "" {
|
||||
reason = fmt.Sprintf("%s (%s)", reason, condition.Message)
|
||||
}
|
||||
dw.Write(printers.Level1, formatRow, ok, formatConditionType(condition), age(condition.LastTransitionTime.Inner.Time), reason)
|
||||
}
|
||||
}
|
||||
|
||||
func writeConcurrencyOptions(dw printers.PrefixWriter, desc *revisionDesc) {
|
||||
dw.WriteColsLn(printers.Level2, "", l("Concurrency"))
|
||||
section := dw.WriteAttribute("Concurrency", "")
|
||||
if desc.concurrencyLimit != nil {
|
||||
dw.WriteColsLn(printers.Level3, "", "", l("Limit"), strconv.FormatInt(*desc.concurrencyLimit, 10))
|
||||
section.WriteAttribute("Limit", strconv.FormatInt(*desc.concurrencyLimit, 10))
|
||||
}
|
||||
if desc.concurrencyTarget != nil {
|
||||
dw.WriteColsLn(printers.Level3, "", "", l("Target"), strconv.Itoa(*desc.concurrencyTarget))
|
||||
section.WriteAttribute("Target", strconv.Itoa(*desc.concurrencyTarget))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -314,36 +287,7 @@ func revisionHeader(desc *revisionDesc) string {
|
|||
return header + " " +
|
||||
"[" + strconv.Itoa(desc.configurationGeneration) + "]" +
|
||||
" " +
|
||||
"(" + age(desc.creationTimestamp) + ")"
|
||||
}
|
||||
|
||||
// Used for conditions table to do own formatting for the table,
|
||||
// as the tabbed writer doesn't work nicely with colors
|
||||
func getMaxTypeLen(conditions duckv1.Conditions) int {
|
||||
max := 0
|
||||
for _, condition := range conditions {
|
||||
if len(condition.Type) > max {
|
||||
max = len(condition.Type)
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// Color the type of the conditions
|
||||
func formatConditionType(condition apis.Condition) string {
|
||||
return string(condition.Type)
|
||||
}
|
||||
|
||||
// Status in ASCII format
|
||||
func formatStatus(status corev1.ConditionStatus) string {
|
||||
switch status {
|
||||
case v1.ConditionTrue:
|
||||
return "++"
|
||||
case v1.ConditionFalse:
|
||||
return "!!"
|
||||
default:
|
||||
return "??"
|
||||
}
|
||||
"(" + commands.Age(desc.creationTimestamp) + ")"
|
||||
}
|
||||
|
||||
// Return either image name with tag or together with its resolved digest
|
||||
|
|
@ -376,47 +320,9 @@ func shortenDigest(digest string) string {
|
|||
return digest
|
||||
}
|
||||
|
||||
var boringDomains = map[string]bool{
|
||||
"serving.knative.dev": true,
|
||||
"client.knative.dev": true,
|
||||
"kubectl.kubernetes.io": true,
|
||||
}
|
||||
|
||||
// Write a map either compact in a single line (possibly truncated) or, if printDetails is set,
|
||||
// over multiple line, one line per key-value pair. The output is sorted by keys.
|
||||
func writeMapDesc(dw printers.PrefixWriter, indent int, m map[string]string, label string, labelPrefix string) {
|
||||
if len(m) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for k := range m {
|
||||
parts := strings.Split(k, "/")
|
||||
if printDetails || len(parts) <= 1 || !boringDomains[parts[0]] {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
if printDetails {
|
||||
l := labelPrefix + label
|
||||
|
||||
for _, key := range keys {
|
||||
dw.WriteColsLn(indent, l, key+"="+m[key])
|
||||
l = labelPrefix
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
dw.WriteColsLn(indent, label, joinAndTruncate(keys, m))
|
||||
}
|
||||
|
||||
// Writer a slice compact (printDetails == false) in one line, or over multiple line
|
||||
// with key-value line-by-line (printDetails == true)
|
||||
func writeSliceDesc(dw printers.PrefixWriter, indent int, s []string, label string, labelPrefix string) {
|
||||
func writeSliceDesc(dw printers.PrefixWriter, s []string, label string, labelPrefix string) {
|
||||
|
||||
if len(s) == 0 {
|
||||
return
|
||||
|
|
@ -425,17 +331,17 @@ func writeSliceDesc(dw printers.PrefixWriter, indent int, s []string, label stri
|
|||
if printDetails {
|
||||
l := labelPrefix + label
|
||||
for _, value := range s {
|
||||
dw.WriteColsLn(indent, l, value)
|
||||
dw.WriteColsLn(l, value)
|
||||
l = labelPrefix
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
joined := strings.Join(s, ", ")
|
||||
if len(joined) > truncateAt {
|
||||
joined = joined[:truncateAt-4] + " ..."
|
||||
if len(joined) > commands.TruncateAt {
|
||||
joined = joined[:commands.TruncateAt-4] + " ..."
|
||||
}
|
||||
dw.WriteColsLn(indent, labelPrefix+label, joined)
|
||||
dw.WriteAttribute(labelPrefix+label, joined)
|
||||
}
|
||||
|
||||
// Write request ... limits or only one of them
|
||||
|
|
@ -453,24 +359,7 @@ func writeResources(dw printers.PrefixWriter, label string, request string, limi
|
|||
return
|
||||
}
|
||||
|
||||
dw.WriteColsLn(printers.Level2, "", l(label), value)
|
||||
}
|
||||
|
||||
// Join to key=value pair, comma separated, and truncate if longer than a limit
|
||||
func joinAndTruncate(sortedKeys []string, m map[string]string) string {
|
||||
ret := ""
|
||||
for _, key := range sortedKeys {
|
||||
ret += fmt.Sprintf("%s=%s, ", key, m[key])
|
||||
if len(ret) > truncateAt {
|
||||
break
|
||||
}
|
||||
}
|
||||
// cut of two latest chars
|
||||
ret = strings.TrimRight(ret, ", ")
|
||||
if len(ret) <= truncateAt {
|
||||
return ret
|
||||
}
|
||||
return string(ret[:truncateAt-4]) + " ..."
|
||||
dw.WriteAttribute(label, value)
|
||||
}
|
||||
|
||||
// Format target percentage that it fits in the revision table
|
||||
|
|
@ -492,13 +381,6 @@ func formatBullet(percentage int64, status corev1.ConditionStatus) string {
|
|||
return fmt.Sprintf("%3d%s", percentage, symbol)
|
||||
}
|
||||
|
||||
func age(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return duration.ShortHumanDuration(time.Now().Sub(t))
|
||||
}
|
||||
|
||||
// Call the backend to query revisions for the given service and build up
|
||||
// the view objects used for output
|
||||
func getRevisionDescriptions(client serving_kn_v1alpha1.KnServingClient, service *v1alpha1.Service, withDetails bool) ([]*revisionDesc, error) {
|
||||
|
|
|
|||
|
|
@ -25,51 +25,60 @@ type flusher interface {
|
|||
Flush() error
|
||||
}
|
||||
|
||||
func NewBarePrefixWriter(out io.Writer) PrefixWriter {
|
||||
return &prefixWriter{out: out, nested: nil, colIndent: 0, spaceIndent: 0}
|
||||
}
|
||||
|
||||
// NewPrefixWriter creates a new PrefixWriter.
|
||||
func NewPrefixWriter(out io.Writer) PrefixWriter {
|
||||
tabWriter := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
||||
return &prefixWriter{out: tabWriter}
|
||||
return &prefixWriter{out: tabWriter, nested: nil, colIndent: 0, spaceIndent: 0}
|
||||
}
|
||||
|
||||
// PrefixWriter can write text at various indentation levels.
|
||||
type PrefixWriter interface {
|
||||
// Write writes text with the specified indentation level.
|
||||
Write(level int, format string, a ...interface{})
|
||||
Writef(format string, a ...interface{})
|
||||
// WriteLine writes an entire line with no indentation level.
|
||||
WriteLine(a ...interface{})
|
||||
// Write columns with an initial indentation
|
||||
WriteCols(level int, cols ...string)
|
||||
WriteCols(cols ...string) PrefixWriter
|
||||
// Write columns with an initial indentation and a newline at the end
|
||||
WriteColsLn(level int, cols ...string)
|
||||
WriteColsLn(cols ...string) PrefixWriter
|
||||
// Flush forces indentation to be reset.
|
||||
Flush() error
|
||||
|
||||
WriteAttribute(attr, value string) PrefixWriter
|
||||
}
|
||||
|
||||
// prefixWriter implements PrefixWriter
|
||||
type prefixWriter struct {
|
||||
out io.Writer
|
||||
out io.Writer
|
||||
nested PrefixWriter
|
||||
colIndent int
|
||||
spaceIndent int
|
||||
}
|
||||
|
||||
var _ PrefixWriter = &prefixWriter{}
|
||||
|
||||
// Each level has 2 spaces for PrefixWriter
|
||||
const (
|
||||
Level0 = iota
|
||||
Level1
|
||||
Level2
|
||||
Level3
|
||||
)
|
||||
|
||||
func (pw *prefixWriter) Write(level int, format string, a ...interface{}) {
|
||||
levelSpace := " "
|
||||
func (pw *prefixWriter) Writef(format string, a ...interface{}) {
|
||||
prefix := ""
|
||||
for i := 0; i < level; i++ {
|
||||
levelSpace := " "
|
||||
for i := 0; i < pw.spaceIndent; i++ {
|
||||
prefix += levelSpace
|
||||
}
|
||||
fmt.Fprintf(pw.out, prefix+format, a...)
|
||||
levelTab := "\t"
|
||||
for i := 0; i < pw.colIndent; i++ {
|
||||
prefix += levelTab
|
||||
}
|
||||
if pw.nested != nil {
|
||||
pw.nested.Writef(prefix+format, a...)
|
||||
} else {
|
||||
fmt.Fprintf(pw.out, prefix+format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) WriteCols(level int, cols ...string) {
|
||||
func (pw *prefixWriter) WriteCols(cols ...string) PrefixWriter {
|
||||
ss := make([]string, len(cols))
|
||||
for i := range cols {
|
||||
ss[i] = "%s"
|
||||
|
|
@ -80,21 +89,37 @@ func (pw *prefixWriter) WriteCols(level int, cols ...string) {
|
|||
s[i] = v
|
||||
}
|
||||
|
||||
pw.Write(level, format, s...)
|
||||
pw.Writef(format, s...)
|
||||
return &prefixWriter{pw.out, pw, 1, 0}
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) WriteColsLn(level int, cols ...string) {
|
||||
pw.WriteCols(level, cols...)
|
||||
// WriteCols writes the columns to the writer and returns a PrefixWriter for
|
||||
// writing any further parts of the "record" in the last column.
|
||||
func (pw *prefixWriter) WriteColsLn(cols ...string) PrefixWriter {
|
||||
ret := pw.WriteCols(cols...)
|
||||
pw.WriteLine()
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) WriteLine(a ...interface{}) {
|
||||
fmt.Fprintln(pw.out, a...)
|
||||
}
|
||||
|
||||
// WriteAttribute writes the attr (as a label) with the given value and returns
|
||||
// a PrefixWriter for writing any subattributes.
|
||||
func (pw *prefixWriter) WriteAttribute(attr, value string) PrefixWriter {
|
||||
pw.WriteColsLn(l(attr), value)
|
||||
return &prefixWriter{pw.out, pw, 0, 1}
|
||||
}
|
||||
|
||||
func (pw *prefixWriter) Flush() error {
|
||||
if f, ok := pw.out.(flusher); ok {
|
||||
return f.Flush()
|
||||
}
|
||||
return fmt.Errorf("output stream %v doesn't support Flush", pw.out)
|
||||
}
|
||||
|
||||
// Format label (extracted so that color could be added more easily to all labels)
|
||||
func l(label string) string {
|
||||
return label + ":"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
// Copyright © 2019 The Knative 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 printers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestWriteColsLn(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := NewBarePrefixWriter(buf)
|
||||
sub := w.WriteColsLn("a", "bbbb", "c", "ddd")
|
||||
sub.WriteColsLn("B", "C", "D", "E")
|
||||
expected := "a\tbbbb\tc\tddd\n\tB\tC\tD\tE\n"
|
||||
actual := buf.String()
|
||||
assert.Equal(t, actual, expected)
|
||||
}
|
||||
|
||||
func TestWriteAttribute(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := NewBarePrefixWriter(buf)
|
||||
sub := w.WriteAttribute("Thing", "Stuff")
|
||||
sub.WriteColsLn("B", "C", "D", "E")
|
||||
expected := "Thing:\tStuff\n B\tC\tD\tE\n"
|
||||
actual := buf.String()
|
||||
assert.Equal(t, actual, expected)
|
||||
}
|
||||
|
||||
func TestWriteNested(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := NewBarePrefixWriter(buf)
|
||||
sub := w.WriteColsLn("*", "Header")
|
||||
subsub := sub.WriteAttribute("Thing", "stuff")
|
||||
subsub.WriteAttribute("Subthing", "substuff")
|
||||
expected := "*\tHeader\n\tThing:\tstuff\n\t Subthing:\tsubstuff\n"
|
||||
actual := buf.String()
|
||||
assert.Equal(t, actual, expected)
|
||||
}
|
||||
Loading…
Reference in New Issue