forked from rook/rook
ReportReconcileResult should never be given an object that is nil. It is impossible to force compile-time checking for this because the object's type is an interface. We can't force this, but we can rework the function definition to handle more cases where the object may not be returned fully-complete, and we can rework callers to return structs (not pointers-to-structs) so it is less likely a caller will pass nil to the function. Signed-off-by: Blaine Gardner <blaine.gardner@redhat.com>
189 lines
6.2 KiB
Go
189 lines
6.2 KiB
Go
/*
|
|
Copyright 2021 The Rook Authors. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
// Reporting focuses on reporting Events, Status Conditions, and the like to users.
|
|
package reporting
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coreos/pkg/capnslog"
|
|
"github.com/pkg/errors"
|
|
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
|
"github.com/stretchr/testify/assert"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
)
|
|
|
|
func Test_objKindOrBestGuess(t *testing.T) {
|
|
var nilCephCluster *cephv1.CephCluster = nil
|
|
|
|
tests := []struct {
|
|
name string
|
|
inObj client.Object
|
|
want string
|
|
}{
|
|
{"CephCluster with API", &cephv1.CephCluster{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "CephCluster",
|
|
},
|
|
}, "CephCluster"},
|
|
{"CephCluster by inference", &cephv1.CephCluster{}, "CephCluster"},
|
|
{"CephCluster with wrong API", &cephv1.CephCluster{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "WRONG",
|
|
},
|
|
}, "WRONG"},
|
|
{"untyped nil", nil, unknownKind},
|
|
{"nil CephCluster", nilCephCluster, "CephCluster"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert.Equal(t, tt.want, objKindOrBestGuess(tt.inObj))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReportReconcileResult(t *testing.T) {
|
|
setupTest := func() (*capnslog.PackageLogger, *bytes.Buffer, *record.FakeRecorder) {
|
|
logBuf := bytes.NewBuffer([]byte{})
|
|
logFmt := capnslog.NewLogFormatter(logBuf, "", 0)
|
|
capnslog.SetFormatter(logFmt)
|
|
logger := capnslog.NewPackageLogger("github.com/rook/rook", "")
|
|
capnslog.SetGlobalLogLevel(capnslog.TRACE)
|
|
|
|
recorder := record.NewFakeRecorder(3)
|
|
|
|
return logger, logBuf, recorder
|
|
}
|
|
|
|
name := "my-cluster"
|
|
namespace := "rook-ceph"
|
|
|
|
cephCluster := &cephv1.CephCluster{
|
|
TypeMeta: metav1.TypeMeta{
|
|
Kind: "CephCluster",
|
|
APIVersion: cephv1.SchemeGroupVersion.String(),
|
|
},
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: namespace,
|
|
},
|
|
}
|
|
|
|
reconcileRequest := reconcile.Request{NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}}
|
|
|
|
successMsg := `successfully configured CephCluster "rook-ceph/my-cluster"`
|
|
successEvent := `Normal ReconcileSucceeded ` + successMsg
|
|
|
|
fakeErr := errors.New("fake-err")
|
|
errorMsg := `failed to reconcile CephCluster "rook-ceph/my-cluster". fake-err`
|
|
errorEvent := `Warning ReconcileFailed ` + errorMsg
|
|
|
|
t.Run("successful reconcile", func(t *testing.T) {
|
|
logger, logBuf, recorder := setupTest()
|
|
|
|
result, err := ReportReconcileResult(logger, recorder, reconcileRequest,
|
|
cephCluster, reconcile.Result{}, nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsZero())
|
|
assert.Equal(t, successMsg, strings.TrimSpace(logBuf.String()))
|
|
assert.Len(t, recorder.Events, 1)
|
|
assert.Equal(t, successEvent, <-recorder.Events)
|
|
})
|
|
|
|
t.Run("reconcile with error", func(t *testing.T) {
|
|
logger, logBuf, recorder := setupTest()
|
|
|
|
result, err := ReportReconcileResult(logger, recorder, reconcileRequest,
|
|
cephCluster, reconcile.Result{}, fakeErr)
|
|
assert.Error(t, err)
|
|
assert.True(t, result.IsZero())
|
|
assert.Equal(t, errorMsg, strings.TrimSpace(logBuf.String()))
|
|
assert.Len(t, recorder.Events, 1)
|
|
assert.Equal(t, errorEvent, <-recorder.Events)
|
|
})
|
|
|
|
t.Run("reconcile with requeue", func(t *testing.T) {
|
|
logger, logBuf, recorder := setupTest()
|
|
|
|
inResult := reconcile.Result{Requeue: true}
|
|
result, err := ReportReconcileResult(logger, recorder, reconcileRequest,
|
|
cephCluster, inResult, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, inResult, result)
|
|
assert.Equal(t, successMsg, strings.TrimSpace(logBuf.String()))
|
|
assert.Len(t, recorder.Events, 1)
|
|
assert.Equal(t, successEvent, <-recorder.Events)
|
|
})
|
|
|
|
t.Run("failed reconcile with requeue", func(t *testing.T) {
|
|
logger, logBuf, recorder := setupTest()
|
|
|
|
inResult := reconcile.Result{RequeueAfter: 567 * time.Second}
|
|
result, err := ReportReconcileResult(logger, recorder, reconcileRequest,
|
|
cephCluster, inResult, fakeErr)
|
|
// this is the trick: when we get a request to requeue after a time _AND_ an error as input,
|
|
// we do not propagate the error to the output, _BUT_ we still record the error in log/event.
|
|
// this is so we can still report errors to users, but the controller-runtime will still
|
|
// obey our desire to reconcile with a delay
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, inResult, result)
|
|
assert.Equal(t, errorMsg, strings.TrimSpace(logBuf.String()))
|
|
assert.Len(t, recorder.Events, 1)
|
|
assert.Equal(t, errorEvent, <-recorder.Events)
|
|
})
|
|
|
|
t.Run("success with empty object", func(t *testing.T) {
|
|
// success returning an empty object might be because a reconcile request is for an
|
|
// already-deleted object
|
|
|
|
logger, logBuf, recorder := setupTest()
|
|
|
|
result, err := ReportReconcileResult(logger, recorder, reconcileRequest,
|
|
&cephv1.CephCluster{}, reconcile.Result{}, nil)
|
|
assert.NoError(t, err)
|
|
assert.True(t, result.IsZero())
|
|
assert.Equal(t, successMsg, strings.TrimSpace(logBuf.String()))
|
|
assert.Len(t, recorder.Events, 1)
|
|
assert.Equal(t, successEvent, <-recorder.Events)
|
|
})
|
|
|
|
t.Run("failure with nil object", func(t *testing.T) {
|
|
// failure with a nil object might be because a reconcile returned nil for the object after
|
|
// an error accidentally
|
|
|
|
logger, logBuf, recorder := setupTest()
|
|
|
|
var obj *cephv1.CephCluster = nil
|
|
|
|
result, err := ReportReconcileResult(logger, recorder, reconcileRequest,
|
|
obj, reconcile.Result{}, fakeErr)
|
|
assert.Error(t, err)
|
|
assert.True(t, result.IsZero())
|
|
assert.Contains(t, logBuf.String(), `object associated with reconcile request CephCluster "rook-ceph/my-cluster" should not be nil`)
|
|
assert.Contains(t, logBuf.String(), errorMsg)
|
|
assert.Len(t, recorder.Events, 1)
|
|
assert.Equal(t, errorEvent, <-recorder.Events)
|
|
})
|
|
}
|