mirror of https://github.com/fluxcd/cli-utils.git
405 lines
11 KiB
Go
405 lines
11 KiB
Go
// Copyright 2020 The Kubernetes Authors.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package json
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
func TestFormatter_FormatApplyEvent(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
previewStrategy common.DryRunStrategy
|
|
event event.ApplyEvent
|
|
applyStats *list.ApplyStats
|
|
statusCollector list.Collector
|
|
expected []map[string]interface{}
|
|
}{
|
|
"resource created without dryrun": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.ApplyEvent{
|
|
Operation: event.Created,
|
|
Type: event.ApplyEventResourceUpdate,
|
|
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,
|
|
Type: event.ApplyEventResourceUpdate,
|
|
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,
|
|
Type: event.ApplyEventResourceUpdate,
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
"completed event": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.ApplyEvent{
|
|
Type: event.ApplyEventCompleted,
|
|
},
|
|
applyStats: &list.ApplyStats{
|
|
ServersideApplied: 1,
|
|
},
|
|
statusCollector: &fakeCollector{
|
|
m: map[object.ObjMetadata]event.StatusEvent{
|
|
object.ObjMetadata{ //nolint:gofmt
|
|
GroupKind: schema.GroupKind{
|
|
Group: "apps",
|
|
Kind: "Deployment",
|
|
},
|
|
Namespace: "foo",
|
|
Name: "my-dep",
|
|
}: {
|
|
Resource: &pollevent.ResourceStatus{
|
|
Status: status.CurrentStatus,
|
|
Message: "Resource is Current",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expected: []map[string]interface{}{
|
|
{
|
|
"configuredCount": 0,
|
|
"count": 1,
|
|
"createdCount": 0,
|
|
"eventType": "completed",
|
|
"failedCount": 0,
|
|
"serverSideCount": 1,
|
|
"type": "apply",
|
|
"unchangedCount": 0,
|
|
"timestamp": "",
|
|
},
|
|
{
|
|
"eventType": "resourceStatus",
|
|
"group": "apps",
|
|
"kind": "Deployment",
|
|
"message": "Resource is Current",
|
|
"name": "my-dep",
|
|
"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.FormatApplyEvent(tc.event, tc.applyStats, tc.statusCollector)
|
|
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
|
|
statusCollector list.Collector
|
|
expected map[string]interface{}
|
|
}{
|
|
"resource update with Current status": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.StatusEvent{
|
|
Type: event.StatusEventResourceUpdate,
|
|
Resource: &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, tc.statusCollector)
|
|
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
|
|
pruneStats *list.PruneStats
|
|
expected map[string]interface{}
|
|
}{
|
|
"resource pruned without dryrun": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.PruneEvent{
|
|
Operation: event.Pruned,
|
|
Type: event.PruneEventResourceUpdate,
|
|
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,
|
|
Type: event.PruneEventResourceUpdate,
|
|
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",
|
|
},
|
|
},
|
|
"prune event with completed status": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.PruneEvent{
|
|
Type: event.PruneEventCompleted,
|
|
},
|
|
pruneStats: &list.PruneStats{
|
|
Pruned: 1,
|
|
Skipped: 2,
|
|
},
|
|
expected: map[string]interface{}{
|
|
"eventType": "completed",
|
|
"skipped": 2,
|
|
"pruned": 1,
|
|
"timestamp": "",
|
|
"type": "prune",
|
|
},
|
|
},
|
|
}
|
|
|
|
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, tc.pruneStats)
|
|
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
|
|
deleteStats *list.DeleteStats
|
|
statusCollector list.Collector
|
|
expected map[string]interface{}
|
|
}{
|
|
"resource deleted without no dryrun": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.DeleteEvent{
|
|
Operation: event.Deleted,
|
|
Type: event.DeleteEventResourceUpdate,
|
|
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,
|
|
Type: event.DeleteEventResourceUpdate,
|
|
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",
|
|
},
|
|
},
|
|
"delete event with completed status": {
|
|
previewStrategy: common.DryRunNone,
|
|
event: event.DeleteEvent{
|
|
Type: event.DeleteEventCompleted,
|
|
},
|
|
deleteStats: &list.DeleteStats{
|
|
Deleted: 1,
|
|
Skipped: 2,
|
|
},
|
|
expected: map[string]interface{}{
|
|
"deleted": 1,
|
|
"eventType": "completed",
|
|
"skipped": 2,
|
|
"timestamp": "",
|
|
"type": "delete",
|
|
},
|
|
},
|
|
}
|
|
|
|
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, tc.deleteStats)
|
|
assert.NoError(t, err)
|
|
|
|
assertOutput(t, tc.expected, out.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
// nolint:unparam
|
|
func assertOutput(t *testing.T, expectedMap map[string]interface{}, actual string) bool {
|
|
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,
|
|
},
|
|
}
|
|
}
|
|
|
|
type fakeCollector struct {
|
|
m map[object.ObjMetadata]event.StatusEvent
|
|
}
|
|
|
|
func (f *fakeCollector) LatestStatus() map[object.ObjMetadata]event.StatusEvent {
|
|
return f.m
|
|
}
|