Skip to content

Commit 318e60f

Browse files
authored
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.
1 parent cfed6a6 commit 318e60f

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package parser
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/sirupsen/logrus"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
)
10+
11+
const (
12+
// TranslationFailureReasonUnknown is used when no specific reason is specified when creating a TranslationFailure.
13+
TranslationFailureReasonUnknown = "unknown"
14+
)
15+
16+
// TranslationFailure represents an error occurring during translating Kubernetes objects into Kong ones.
17+
// It can be associated with one or more Kubernetes objects.
18+
type TranslationFailure struct {
19+
causingObjects []client.Object
20+
reason string
21+
}
22+
23+
// NewTranslationFailure creates a TranslationFailure with a reason that should be a human-readable explanation
24+
// of the error reason, and a causingObjects slice that specifies what objects have caused the error.
25+
func NewTranslationFailure(reason string, causingObjects ...client.Object) (TranslationFailure, error) {
26+
if reason == "" {
27+
reason = TranslationFailureReasonUnknown
28+
}
29+
if len(causingObjects) < 1 {
30+
return TranslationFailure{}, fmt.Errorf("no causing objects specified, reason: %s", reason)
31+
}
32+
33+
return TranslationFailure{
34+
causingObjects: causingObjects,
35+
reason: reason,
36+
}, nil
37+
}
38+
39+
// CausingObjects returns a slice of objects that have caused the translation error.
40+
func (p TranslationFailure) CausingObjects() []client.Object {
41+
return p.causingObjects
42+
}
43+
44+
// Reason returns a human-readable reason of the failure.
45+
func (p TranslationFailure) Reason() string {
46+
return p.reason
47+
}
48+
49+
// TranslationFailuresCollector should be used to collect all translation failures that happen during the translation process.
50+
type TranslationFailuresCollector struct {
51+
failures []TranslationFailure
52+
logger logrus.FieldLogger
53+
}
54+
55+
func NewTranslationFailuresCollector(logger logrus.FieldLogger) (*TranslationFailuresCollector, error) {
56+
if logger == nil {
57+
return nil, errors.New("missing logger")
58+
}
59+
return &TranslationFailuresCollector{logger: logger}, nil
60+
}
61+
62+
// PushTranslationFailure registers a translation failure.
63+
func (c *TranslationFailuresCollector) PushTranslationFailure(reason string, causingObjects ...client.Object) {
64+
translationErr, err := NewTranslationFailure(reason, causingObjects...)
65+
if err != nil {
66+
c.logger.Warningf("failed to create translation failure: %w", err)
67+
return
68+
}
69+
70+
c.failures = append(c.failures, translationErr)
71+
}
72+
73+
// PopTranslationFailures returns all translation failures that occurred during the translation process and erases them
74+
// in the collector. It makes the collector reusable during next translation runs.
75+
func (c *TranslationFailuresCollector) PopTranslationFailures() []TranslationFailure {
76+
errs := c.failures
77+
c.failures = nil
78+
79+
return errs
80+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package parser_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/sirupsen/logrus"
7+
"github.com/sirupsen/logrus/hooks/test"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/parser"
13+
kongv1 "github.com/kong/kubernetes-ingress-controller/v2/pkg/apis/configuration/v1"
14+
)
15+
16+
const someValidTranslationFailureReason = "some valid reason"
17+
18+
func someValidTranslationFailureCausingObjects() []client.Object {
19+
return []client.Object{&kongv1.KongIngress{}, &kongv1.KongPlugin{}}
20+
}
21+
22+
func TestTranslationFailure(t *testing.T) {
23+
t.Run("is created and returns reason and causing objects", func(t *testing.T) {
24+
transErr, err := parser.NewTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()...)
25+
require.NoError(t, err)
26+
27+
assert.Equal(t, someValidTranslationFailureReason, transErr.Reason())
28+
assert.ElementsMatch(t, someValidTranslationFailureCausingObjects(), transErr.CausingObjects())
29+
})
30+
31+
t.Run("fallbacks to unknown reason when empty", func(t *testing.T) {
32+
transErr, err := parser.NewTranslationFailure("", someValidTranslationFailureCausingObjects()...)
33+
require.NoError(t, err)
34+
require.Equal(t, parser.TranslationFailureReasonUnknown, transErr.Reason())
35+
})
36+
37+
t.Run("requires at least one causing object", func(t *testing.T) {
38+
_, err := parser.NewTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()[0])
39+
require.NoError(t, err)
40+
41+
_, err = parser.NewTranslationFailure(someValidTranslationFailureReason)
42+
require.Error(t, err)
43+
})
44+
}
45+
46+
func TestTranslationFailuresCollector(t *testing.T) {
47+
testLogger, _ := test.NewNullLogger()
48+
49+
t.Run("is created when logger valid", func(t *testing.T) {
50+
collector, err := parser.NewTranslationFailuresCollector(testLogger)
51+
require.NoError(t, err)
52+
require.NotNil(t, collector)
53+
})
54+
55+
t.Run("requires non nil logger", func(t *testing.T) {
56+
_, err := parser.NewTranslationFailuresCollector(nil)
57+
require.Error(t, err)
58+
})
59+
60+
t.Run("pushes and pops translation failures", func(t *testing.T) {
61+
collector, err := parser.NewTranslationFailuresCollector(testLogger)
62+
require.NoError(t, err)
63+
64+
collector.PushTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()...)
65+
collector.PushTranslationFailure(someValidTranslationFailureReason, someValidTranslationFailureCausingObjects()...)
66+
67+
collectedErrors := collector.PopTranslationFailures()
68+
require.Len(t, collectedErrors, 2)
69+
require.Empty(t, collector.PopTranslationFailures(), "second call should not return any failure")
70+
})
71+
72+
t.Run("does not crash but logs warning when no causing objects passed", func(t *testing.T) {
73+
logger, loggerHook := test.NewNullLogger()
74+
collector, err := parser.NewTranslationFailuresCollector(logger)
75+
require.NoError(t, err)
76+
77+
collector.PushTranslationFailure(someValidTranslationFailureReason)
78+
79+
lastLog := loggerHook.LastEntry()
80+
require.NotNil(t, lastLog)
81+
require.Equal(t, logrus.WarnLevel, lastLog.Level)
82+
require.Len(t, collector.PopTranslationFailures(), 0, "no failures expected - causing objects missing")
83+
})
84+
}

0 commit comments

Comments
 (0)