mirror of https://github.com/fluxcd/cli-utils.git
commit
3fe57dde29
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue