mirror of https://github.com/fluxcd/cli-utils.git
609 lines
16 KiB
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,
|
|
},
|
|
}
|
|
}
|