From 318e60f16609dd4df256a0a97002da2e02f7856c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20Burzy=C5=84ski?= Date: Thu, 27 Oct 2022 17:10:13 +0200 Subject: [PATCH] feat(#3097): add TranslationFailure and TranslationFailuresCollector with unit tests (#3110) Introduces utility types that are going to be used for collecting failures that happen during the translation process. --- internal/dataplane/parser/failures.go | 80 +++++++++++++++++++++ internal/dataplane/parser/failures_test.go | 84 ++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 internal/dataplane/parser/failures.go create mode 100644 internal/dataplane/parser/failures_test.go diff --git a/internal/dataplane/parser/failures.go b/internal/dataplane/parser/failures.go new file mode 100644 index 0000000000..f8ea49af9b --- /dev/null +++ b/internal/dataplane/parser/failures.go @@ -0,0 +1,80 @@ +package parser + +import ( + "errors" + "fmt" + + "github.com/sirupsen/logrus" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + // TranslationFailureReasonUnknown is used when no specific reason is specified when creating a TranslationFailure. + TranslationFailureReasonUnknown = "unknown" +) + +// TranslationFailure represents an error occurring during translating Kubernetes objects into Kong ones. +// It can be associated with one or more Kubernetes objects. +type TranslationFailure struct { + causingObjects []client.Object + reason string +} + +// NewTranslationFailure creates a TranslationFailure with a reason that should be a human-readable explanation +// of the error reason, and a causingObjects slice that specifies what objects have caused the error. +func NewTranslationFailure(reason string, causingObjects ...client.Object) (TranslationFailure, error) { + if reason == "" { + reason = TranslationFailureReasonUnknown + } + if len(causingObjects) < 1 { + return TranslationFailure{}, fmt.Errorf("no causing objects specified, reason: %s", reason) + } + + return TranslationFailure{ + causingObjects: causingObjects, + reason: reason, + }, nil +} + +// CausingObjects returns a slice of objects that have caused the translation error. +func (p TranslationFailure) CausingObjects() []client.Object { + return p.causingObjects +} + +// Reason returns a human-readable reason of the failure. +func (p TranslationFailure) Reason() string { + return p.reason +} + +// TranslationFailuresCollector should be used to collect all translation failures that happen during the translation process. +type TranslationFailuresCollector struct { + failures []TranslationFailure + logger logrus.FieldLogger +} + +func NewTranslationFailuresCollector(logger logrus.FieldLogger) (*TranslationFailuresCollector, error) { + if logger == nil { + return nil, errors.New("missing logger") + } + return &TranslationFailuresCollector{logger: logger}, nil +} + +// PushTranslationFailure registers a translation failure. +func (c *TranslationFailuresCollector) PushTranslationFailure(reason string, causingObjects ...client.Object) { + translationErr, err := NewTranslationFailure(reason, causingObjects...) + if err != nil { + c.logger.Warningf("failed to create translation failure: %w", err) + return + } + + c.failures = append(c.failures, translationErr) +} + +// PopTranslationFailures returns all translation failures that occurred during the translation process and erases them +// in the collector. It makes the collector reusable during next translation runs. +func (c *TranslationFailuresCollector) PopTranslationFailures() []TranslationFailure { + errs := c.failures + c.failures = nil + + return errs +} diff --git a/internal/dataplane/parser/failures_test.go b/internal/dataplane/parser/failures_test.go new file mode 100644 index 0000000000..ac23a75901 --- /dev/null +++ b/internal/dataplane/parser/failures_test.go @@ -0,0 +1,84 @@ +package parser_test + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser" + kongv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1" +) + +const someValidTranslationFailureReason = "some valid reason" + +func someValidTranslationFailureCausingObjects() []client.Object { + return []client.Object{&kongv1.KongIngress{}, &kongv1.KongPlugin{}} +} + +func TestTranslationFailure(t *testing.T) { + t.Run("is created and returns reason and causing objects", func(t *testing.T) { + transErr, err := parser.NewTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()...) + require.NoError(t, err) + + assert.Equal(t, someValidTranslationFailureReason, transErr.Reason()) + assert.ElementsMatch(t, someValidTranslationFailureCausingObjects(), transErr.CausingObjects()) + }) + + t.Run("fallbacks to unknown reason when empty", func(t *testing.T) { + transErr, err := parser.NewTranslationFailure("", someValidTranslationFailureCausingObjects()...) + require.NoError(t, err) + require.Equal(t, parser.TranslationFailureReasonUnknown, transErr.Reason()) + }) + + t.Run("requires at least one causing object", func(t *testing.T) { + _, err := parser.NewTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()[0]) + require.NoError(t, err) + + _, err = parser.NewTranslationFailure(someValidTranslationFailureReason) + require.Error(t, err) + }) +} + +func TestTranslationFailuresCollector(t *testing.T) { + testLogger, _ := test.NewNullLogger() + + t.Run("is created when logger valid", func(t *testing.T) { + collector, err := parser.NewTranslationFailuresCollector(testLogger) + require.NoError(t, err) + require.NotNil(t, collector) + }) + + t.Run("requires non nil logger", func(t *testing.T) { + _, err := parser.NewTranslationFailuresCollector(nil) + require.Error(t, err) + }) + + t.Run("pushes and pops translation failures", func(t *testing.T) { + collector, err := parser.NewTranslationFailuresCollector(testLogger) + require.NoError(t, err) + + collector.PushTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()...) + collector.PushTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()...) + + collectedErrors := collector.PopTranslationFailures() + require.Len(t, collectedErrors, 2) + require.Empty(t, collector.PopTranslationFailures(), "second call should not return any failure") + }) + + t.Run("does not crash but logs warning when no causing objects passed", func(t *testing.T) { + logger, loggerHook := test.NewNullLogger() + collector, err := parser.NewTranslationFailuresCollector(logger) + require.NoError(t, err) + + collector.PushTranslationFailure(someValidTranslationFailureReason) + + lastLog := loggerHook.LastEntry() + require.NotNil(t, lastLog) + require.Equal(t, logrus.WarnLevel, lastLog.Level) + require.Len(t, collector.PopTranslationFailures(), 0, "no failures expected - causing objects missing") + }) +}