Skip to content

Commit

Permalink
feat(#3097): add TranslationFailure and TranslationFailuresCollector …
Browse files Browse the repository at this point in the history
…with unit tests (#3110)

Introduces utility types that are going to be used for collecting failures that happen during the translation process.
  • Loading branch information
czeslavo committed Oct 27, 2022
1 parent cfed6a6 commit 318e60f
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 0 deletions.
80 changes: 80 additions & 0 deletions internal/dataplane/parser/failures.go
Original file line number Diff line number Diff line change
@@ -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
}
84 changes: 84 additions & 0 deletions internal/dataplane/parser/failures_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}

0 comments on commit 318e60f

Please sign in to comment.