cli-utils/pkg/printers/json/formatter_test.go

609 lines
16 KiB
Go

// Copyright 2020 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package json
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
"sigs.k8s.io/cli-utils/pkg/apply/event"
"sigs.k8s.io/cli-utils/pkg/common"
pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
"sigs.k8s.io/cli-utils/pkg/kstatus/status"
"sigs.k8s.io/cli-utils/pkg/object"
"sigs.k8s.io/cli-utils/pkg/print/list"
"sigs.k8s.io/cli-utils/pkg/print/stats"
)
func TestFormatter_FormatApplyEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.ApplyEvent
expected []map[string]interface{}
}{
"resource created without dryrun": {
previewStrategy: common.DryRunNone,
event: event.ApplyEvent{
Operation: event.Created,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceApplied",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Created",
"timestamp": "",
"type": "apply",
},
},
},
"resource updated with client dryrun": {
previewStrategy: common.DryRunClient,
event: event.ApplyEvent{
Operation: event.Configured,
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceApplied",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"operation": "Configured",
"timestamp": "",
"type": "apply",
},
},
},
"resource updated with server dryrun": {
previewStrategy: common.DryRunServer,
event: event.ApplyEvent{
Operation: event.Configured,
Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceApplied",
"group": "batch",
"kind": "CronJob",
"name": "my-cron",
"namespace": "foo",
"operation": "Configured",
"timestamp": "",
"type": "apply",
},
},
},
"resource apply error": {
previewStrategy: common.DryRunNone,
event: event.ApplyEvent{
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
Error: errors.New("example error"),
},
expected: []map[string]interface{}{
{
"eventType": "resourceFailed",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"timestamp": "",
"type": "apply",
"error": "example error",
},
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatApplyEvent(tc.event)
assert.NoError(t, err)
objects := strings.Split(strings.TrimSpace(out.String()), "\n")
if !assert.Equal(t, len(tc.expected), len(objects)) {
t.FailNow()
}
for i := range tc.expected {
assertOutput(t, tc.expected[i], objects[i])
}
})
}
}
func TestFormatter_FormatStatusEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.StatusEvent
expected map[string]interface{}
}{
"resource update with Current status": {
previewStrategy: common.DryRunNone,
event: event.StatusEvent{
Identifier: object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Namespace: "foo",
Name: "bar",
},
PollResourceInfo: &pollevent.ResourceStatus{
Identifier: object.ObjMetadata{
GroupKind: schema.GroupKind{
Group: "apps",
Kind: "Deployment",
},
Namespace: "foo",
Name: "bar",
},
Status: status.CurrentStatus,
Message: "Resource is Current",
},
},
expected: map[string]interface{}{
"eventType": "resourceStatus",
"group": "apps",
"kind": "Deployment",
"message": "Resource is Current",
"name": "bar",
"namespace": "foo",
"status": "Current",
"timestamp": "",
"type": "status",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatStatusEvent(tc.event)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
func TestFormatter_FormatPruneEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.PruneEvent
expected map[string]interface{}
}{
"resource pruned without dryrun": {
previewStrategy: common.DryRunNone,
event: event.PruneEvent{
Operation: event.Pruned,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourcePruned",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Pruned",
"timestamp": "",
"type": "prune",
},
},
"resource skipped with client dryrun": {
previewStrategy: common.DryRunClient,
event: event.PruneEvent{
Operation: event.PruneSkipped,
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourcePruned",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"operation": "PruneSkipped",
"timestamp": "",
"type": "prune",
},
},
"resource prune error": {
previewStrategy: common.DryRunNone,
event: event.PruneEvent{
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
Error: errors.New("example error"),
},
expected: map[string]interface{}{
"eventType": "resourceFailed",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"timestamp": "",
"type": "prune",
"error": "example error",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatPruneEvent(tc.event)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
func TestFormatter_FormatDeleteEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.DeleteEvent
statusCollector list.Collector
expected map[string]interface{}
}{
"resource deleted without no dryrun": {
previewStrategy: common.DryRunNone,
event: event.DeleteEvent{
Operation: event.Deleted,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceDeleted",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Deleted",
"timestamp": "",
"type": "delete",
},
},
"resource skipped with client dryrun": {
previewStrategy: common.DryRunClient,
event: event.DeleteEvent{
Operation: event.DeleteSkipped,
Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceDeleted",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "",
"operation": "DeleteSkipped",
"timestamp": "",
"type": "delete",
},
},
"resource delete error": {
previewStrategy: common.DryRunNone,
event: event.DeleteEvent{
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
Error: errors.New("example error"),
},
expected: map[string]interface{}{
"eventType": "resourceFailed",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"timestamp": "",
"type": "delete",
"error": "example error",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatDeleteEvent(tc.event)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
func TestFormatter_FormatWaitEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.WaitEvent
statusCollector list.Collector
expected map[string]interface{}
}{
"resource reconciled": {
previewStrategy: common.DryRunNone,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.Reconciled,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Reconciled",
"timestamp": "",
"type": "wait",
},
},
"resource reconciled (client-side dry-run)": {
previewStrategy: common.DryRunClient,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.Reconciled,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Reconciled",
"timestamp": "",
"type": "wait",
},
},
"resource reconciled (server-side dry-run)": {
previewStrategy: common.DryRunServer,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.Reconciled,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Reconciled",
"timestamp": "",
"type": "wait",
},
},
"resource reconcile pending": {
previewStrategy: common.DryRunServer,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.ReconcilePending,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Pending",
"timestamp": "",
"type": "wait",
},
},
"resource reconcile skipped": {
previewStrategy: common.DryRunServer,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.ReconcileSkipped,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Skipped",
"timestamp": "",
"type": "wait",
},
},
"resource reconcile timeout": {
previewStrategy: common.DryRunServer,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.ReconcileTimeout,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Timeout",
"timestamp": "",
"type": "wait",
},
},
"resource reconcile failed": {
previewStrategy: common.DryRunNone,
event: event.WaitEvent{
GroupName: "wait-1",
Operation: event.ReconcileFailed,
Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
},
expected: map[string]interface{}{
"eventType": "resourceReconciled",
"group": "apps",
"kind": "Deployment",
"name": "my-dep",
"namespace": "default",
"operation": "Failed",
"timestamp": "",
"type": "wait",
},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatWaitEvent(tc.event)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
func TestFormatter_FormatActionGroupEvent(t *testing.T) {
testCases := map[string]struct {
previewStrategy common.DryRunStrategy
event event.ActionGroupEvent
actionGroups []event.ActionGroup
statsCollector stats.Stats
statusCollector list.Collector
expected map[string]interface{}
}{
"not the last apply action group finished": {
previewStrategy: common.DryRunNone,
event: event.ActionGroupEvent{
GroupName: "age-1",
Action: event.ApplyAction,
Type: event.Finished,
},
actionGroups: []event.ActionGroup{
{
Name: "age-1",
Action: event.ApplyAction,
},
{
Name: "age-2",
Action: event.ApplyAction,
},
},
expected: map[string]interface{}{},
},
"the last apply action group finished": {
previewStrategy: common.DryRunNone,
event: event.ActionGroupEvent{
GroupName: "age-2",
Action: event.ApplyAction,
Type: event.Finished,
},
actionGroups: []event.ActionGroup{
{
Name: "age-1",
Action: event.ApplyAction,
},
{
Name: "age-2",
Action: event.ApplyAction,
},
},
statsCollector: stats.Stats{
ApplyStats: stats.ApplyStats{
ServersideApplied: 42,
},
},
expected: map[string]interface{}{
"eventType": "completed",
"configuredCount": 0,
"count": 42,
"createdCount": 0,
"failedCount": 0,
"serverSideCount": 42,
"timestamp": "2022-01-06T05:22:48Z",
"type": "apply",
"unchangedCount": 0,
},
},
"last prune action group started": {
previewStrategy: common.DryRunNone,
event: event.ActionGroupEvent{
GroupName: "age-2",
Action: event.PruneAction,
Type: event.Started,
},
actionGroups: []event.ActionGroup{
{
Name: "age-1",
Action: event.PruneAction,
},
{
Name: "age-2",
Action: event.PruneAction,
},
},
expected: map[string]interface{}{},
},
}
for tn, tc := range testCases {
t.Run(tn, func(t *testing.T) {
ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
formatter := NewFormatter(ioStreams, tc.previewStrategy)
err := formatter.FormatActionGroupEvent(tc.event, tc.actionGroups, tc.statsCollector, tc.statusCollector)
assert.NoError(t, err)
assertOutput(t, tc.expected, out.String())
})
}
}
// nolint:unparam
func assertOutput(t *testing.T, expectedMap map[string]interface{}, actual string) bool {
if len(expectedMap) == 0 {
return assert.Empty(t, actual)
}
var m map[string]interface{}
err := json.Unmarshal([]byte(actual), &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
}
}
for key, val := range m {
if floatVal, ok := val.(float64); ok {
m[key] = int(floatVal)
}
}
return assert.Equal(t, expectedMap, m)
}
func createIdentifier(group, kind, namespace, name string) object.ObjMetadata {
return object.ObjMetadata{
Namespace: namespace,
Name: name,
GroupKind: schema.GroupKind{
Group: group,
Kind: kind,
},
}
}