Merge pull request #603 from chunglu-chou/kpt-json

Add json printer
This commit is contained in:
Kubernetes Prow Robot 2022-08-15 10:06:14 -07:00 committed by GitHub
commit 3fe57dde29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 453 additions and 14 deletions

View File

@ -6,6 +6,7 @@ package status
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"
"time"
@ -53,6 +54,23 @@ metadata:
}
)
type fakePoller struct {
events []pollevent.Event
}
func (f *fakePoller) Poll(ctx context.Context, _ object.ObjMetadataSet,
_ polling.PollOptions) <-chan pollevent.Event {
eventChannel := make(chan pollevent.Event)
go func() {
defer close(eventChannel)
for _, e := range f.events {
eventChannel <- e
}
<-ctx.Done()
}()
return eventChannel
}
func TestCommand(t *testing.T) {
testCases := map[string]struct {
pollUntil string
@ -217,6 +235,261 @@ foo/deployment.apps/default/foo is InProgress: inProgress
},
}
jsonTestCases := map[string]struct {
pollUntil string
printer string
timeout time.Duration
input string
inventory object.ObjMetadataSet
events []pollevent.Event
expectedErrMsg string
expectedOutput []map[string]interface{}
}{
"wait for all known json": {
pollUntil: "known",
printer: "json",
input: inventoryTemplate,
inventory: object.ObjMetadataSet{
depObject,
stsObject,
},
events: []pollevent.Event{
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: depObject,
Status: status.InProgressStatus,
Message: "inProgress",
},
},
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: stsObject,
Status: status.CurrentStatus,
Message: "current",
},
},
},
expectedOutput: []map[string]interface{}{
{
"group": "apps",
"kind": "Deployment",
"namespace": "default",
"name": "foo",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "InProgress",
"message": "inProgress",
},
{
"group": "apps",
"kind": "StatefulSet",
"namespace": "default",
"name": "bar",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "Current",
"message": "current",
},
},
},
"wait for all current json": {
pollUntil: "current",
printer: "json",
input: inventoryTemplate,
inventory: object.ObjMetadataSet{
depObject,
stsObject,
},
events: []pollevent.Event{
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: depObject,
Status: status.InProgressStatus,
Message: "inProgress",
},
},
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: stsObject,
Status: status.InProgressStatus,
Message: "inProgress",
},
},
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: stsObject,
Status: status.CurrentStatus,
Message: "current",
},
},
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: depObject,
Status: status.CurrentStatus,
Message: "current",
},
},
},
expectedOutput: []map[string]interface{}{
{
"group": "apps",
"kind": "Deployment",
"namespace": "default",
"name": "foo",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "InProgress",
"message": "inProgress",
},
{
"group": "apps",
"kind": "StatefulSet",
"namespace": "default",
"name": "bar",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "InProgress",
"message": "inProgress",
},
{
"group": "apps",
"kind": "StatefulSet",
"namespace": "default",
"name": "bar",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "Current",
"message": "current",
},
{
"group": "apps",
"kind": "Deployment",
"namespace": "default",
"name": "foo",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "Current",
"message": "current",
},
},
},
"wait for all deleted json": {
pollUntil: "deleted",
printer: "json",
input: inventoryTemplate,
inventory: object.ObjMetadataSet{
depObject,
stsObject,
},
events: []pollevent.Event{
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: stsObject,
Status: status.NotFoundStatus,
Message: "notFound",
},
},
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: depObject,
Status: status.NotFoundStatus,
Message: "notFound",
},
},
},
expectedOutput: []map[string]interface{}{
{
"group": "apps",
"kind": "StatefulSet",
"namespace": "default",
"name": "bar",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "NotFound",
"message": "notFound",
},
{
"group": "apps",
"kind": "Deployment",
"namespace": "default",
"name": "foo",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "NotFound",
"message": "notFound",
},
},
},
"forever with timeout json": {
pollUntil: "forever",
printer: "json",
timeout: 2 * time.Second,
input: inventoryTemplate,
inventory: object.ObjMetadataSet{
depObject,
stsObject,
},
events: []pollevent.Event{
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: stsObject,
Status: status.InProgressStatus,
Message: "inProgress",
},
},
{
Type: pollevent.ResourceUpdateEvent,
Resource: &pollevent.ResourceStatus{
Identifier: depObject,
Status: status.InProgressStatus,
Message: "inProgress",
},
},
},
expectedOutput: []map[string]interface{}{
{
"group": "apps",
"kind": "StatefulSet",
"namespace": "default",
"name": "bar",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "InProgress",
"message": "inProgress",
},
{
"group": "apps",
"kind": "Deployment",
"namespace": "default",
"name": "foo",
"timestamp": "",
"type": "status",
"inventory-name": "foo",
"status": "InProgress",
"message": "inProgress",
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
@ -258,21 +531,75 @@ foo/deployment.apps/default/foo is InProgress: inProgress
assert.Equal(t, strings.TrimSpace(buf.String()), strings.TrimSpace(tc.expectedOutput))
})
}
for tn, tc := range jsonTestCases {
t.Run(tn, func(t *testing.T) {
tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
defer tf.Cleanup()
loader := manifestreader.NewFakeLoader(tf, tc.inventory)
runner := &Runner{
factory: tf,
invFactory: inventory.FakeClientFactory(tc.inventory),
loader: loader,
pollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) {
return &fakePoller{tc.events}, nil
},
pollUntil: tc.pollUntil,
output: tc.printer,
timeout: tc.timeout,
}
cmd := &cobra.Command{
RunE: runner.runE,
}
cmd.SetIn(strings.NewReader(tc.input))
var buf bytes.Buffer
cmd.SetOut(&buf)
cmd.SetArgs([]string{})
err := cmd.Execute()
if tc.expectedErrMsg != "" {
if !assert.Error(t, err) {
t.FailNow()
}
assert.Contains(t, err.Error(), tc.expectedErrMsg)
return
}
assert.NoError(t, err)
actual := strings.Split(buf.String(), "\n")
assertOutput(t, tc.expectedOutput, actual)
})
}
}
type fakePoller struct {
events []pollevent.Event
}
func (f *fakePoller) Poll(ctx context.Context, _ object.ObjMetadataSet,
_ polling.PollOptions) <-chan pollevent.Event {
eventChannel := make(chan pollevent.Event)
go func() {
defer close(eventChannel)
for _, e := range f.events {
eventChannel <- e
// nolint:unparam
func assertOutput(t *testing.T, expectedOutput []map[string]interface{}, actual []string) bool {
for i, expectedMap := range expectedOutput {
if len(expectedMap) == 0 {
return assert.Empty(t, actual[i])
}
<-ctx.Done()
}()
return eventChannel
var m map[string]interface{}
err := json.Unmarshal([]byte(actual[i]), &m)
if !assert.NoError(t, err) {
return false
}
if _, found := expectedMap["timestamp"]; found {
if _, ok := m["timestamp"]; ok {
delete(expectedMap, "timestamp")
delete(m, "timestamp")
} else {
t.Error("expected to find key 'timestamp', but didn't")
return false
}
}
if !assert.Equal(t, expectedMap, m) {
return false
}
}
return true
}

View File

@ -0,0 +1,109 @@
// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package json
import (
"encoding/json"
"fmt"
"strings"
"time"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/cmd/status/printers/printer"
"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common"
"sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector"
pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/list"
jsonprinter "sigs.k8s.io/cli-utils/pkg/printers/json"
)
// Printer implements the Printer interface and outputs the resource
// status information as a list of events as they happen.
type Printer struct {
Formatter list.Formatter
IOStreams genericclioptions.IOStreams
Data *printer.PrintData
}
// NewPrinter returns a new instance of the eventPrinter.
func NewPrinter(ioStreams genericclioptions.IOStreams, printData *printer.PrintData) *Printer {
return &Printer{
Formatter: jsonprinter.NewFormatter(ioStreams, common.DryRunNone),
IOStreams: ioStreams,
Data: printData,
}
}
// Print takes an event channel and outputs the status events on the channel
// until the channel is closed. The provided cancelFunc is consulted on
// every event and is responsible for stopping the poller when appropriate.
// This function will block.
func (ep *Printer) Print(ch <-chan pollevent.Event, identifiers object.ObjMetadataSet,
cancelFunc collector.ObserverFunc) error {
coll := collector.NewResourceStatusCollector(identifiers)
// The actual work is done by the collector, which will invoke the
// callback on every event. In the callback we print the status
// information and call the cancelFunc which is responsible for
// stopping the poller at the correct time.
done := coll.ListenWithObserver(ch, collector.ObserverFunc(
func(statusCollector *collector.ResourceStatusCollector, e pollevent.Event) {
err := ep.printStatusEvent(e)
if err != nil {
panic(err)
}
cancelFunc(statusCollector, e)
}),
)
// Listen to the channel until it is closed.
var err error
for msg := range done {
err = msg.Err
}
return err
}
func (ep *Printer) printStatusEvent(se pollevent.Event) error {
switch se.Type {
case pollevent.ResourceUpdateEvent:
id := se.Resource.Identifier
var invName string
var ok bool
if invName, ok = ep.Data.InvNameMap[id]; !ok {
return fmt.Errorf("%s: resource not found", id)
}
// filter out status that are not assigned
statusString := se.Resource.Status.String()
if _, ok := ep.Data.StatusSet[strings.ToLower(statusString)]; len(ep.Data.StatusSet) != 0 && !ok {
return nil
}
eventInfo := ep.createJSONObj(id)
eventInfo["inventory-name"] = invName
eventInfo["status"] = statusString
eventInfo["message"] = se.Resource.Message
b, err := json.Marshal(eventInfo)
if err != nil {
return err
}
_, err = fmt.Fprintf(ep.IOStreams.Out, "%s\n", string(b))
return err
case pollevent.ErrorEvent:
return ep.Formatter.FormatErrorEvent(event.ErrorEvent{
Err: se.Error,
})
}
return nil
}
func (ep *Printer) createJSONObj(id object.ObjMetadata) map[string]interface{} {
return map[string]interface{}{
"group": id.GroupKind.Group,
"kind": id.GroupKind.Kind,
"namespace": id.Namespace,
"name": id.Name,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"type": "status",
}
}

View File

@ -6,6 +6,7 @@ package printers
import (
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/cmd/status/printers/event"
"sigs.k8s.io/cli-utils/cmd/status/printers/json"
"sigs.k8s.io/cli-utils/cmd/status/printers/printer"
"sigs.k8s.io/cli-utils/cmd/status/printers/table"
)
@ -16,6 +17,8 @@ func CreatePrinter(printerType string, ioStreams genericclioptions.IOStreams, pr
switch printerType {
case "table":
return table.NewPrinter(ioStreams, printData), nil
case "json":
return json.NewPrinter(ioStreams, printData), nil
default:
return event.NewPrinter(ioStreams, printData), nil
}