From 24efc80bcf316aa08ff6d8bc0af963e0657872a6 Mon Sep 17 00:00:00 2001 From: Giovanni Liva Date: Mon, 21 Nov 2022 12:52:49 +0100 Subject: [PATCH] fix(operator): use correct parent/child span relationship (#418) Co-authored-by: RealAnna Fixes https://github.com/keptn/lifecycle-toolkit/issues/336 --- operator/controllers/common/ITracer.go | 2 - .../common/fake/spanhandler_mock.go | 141 +++++++++++ operator/controllers/common/phasehandler.go | 20 +- operator/controllers/common/spanhandler.go | 6 + operator/controllers/common/test_utils.go | 38 +++ .../controllers/keptnapp/controller_test.go | 41 +--- .../controllers/keptnappversion/controller.go | 52 +++-- .../keptnappversion/controller_test.go | 219 ++++++++++++++++++ operator/test/component/appcontroller_test.go | 32 ++- 9 files changed, 470 insertions(+), 81 deletions(-) create mode 100644 operator/controllers/common/fake/spanhandler_mock.go create mode 100644 operator/controllers/common/test_utils.go create mode 100644 operator/controllers/keptnappversion/controller_test.go diff --git a/operator/controllers/common/ITracer.go b/operator/controllers/common/ITracer.go index 23b875e112..4684a89928 100644 --- a/operator/controllers/common/ITracer.go +++ b/operator/controllers/common/ITracer.go @@ -2,7 +2,5 @@ package common import "go.opentelemetry.io/otel/trace" -// TODO Autogenerated mock uses pointers, I changed it manually - //go:generate moq -pkg fake -skip-ensure -out ./fake/tracer.go . ITracer type ITracer = trace.Tracer diff --git a/operator/controllers/common/fake/spanhandler_mock.go b/operator/controllers/common/fake/spanhandler_mock.go new file mode 100644 index 0000000000..71406bd529 --- /dev/null +++ b/operator/controllers/common/fake/spanhandler_mock.go @@ -0,0 +1,141 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package fake + +import ( + "context" + "go.opentelemetry.io/otel/trace" + "sigs.k8s.io/controller-runtime/pkg/client" + "sync" +) + +// SpanHandlerIMock is a mock implementation of common.SpanHandlerI. +// +// func TestSomethingThatUsesSpanHandlerI(t *testing.T) { +// +// // make and configure a mocked common.SpanHandlerI +// mockedSpanHandlerI := &SpanHandlerIMock{ +// GetSpanFunc: func(ctx context.Context, tracer trace.Tracer, reconcileObject client.Object, phase string) (context.Context, trace.Span, error) { +// panic("mock out the GetSpan method") +// }, +// UnbindSpanFunc: func(reconcileObject client.Object, phase string) error { +// panic("mock out the UnbindSpan method") +// }, +// } +// +// // use mockedSpanHandlerI in code that requires common.SpanHandlerI +// // and then make assertions. +// +// } +type SpanHandlerIMock struct { + // GetSpanFunc mocks the GetSpan method. + GetSpanFunc func(ctx context.Context, tracer trace.Tracer, reconcileObject client.Object, phase string) (context.Context, trace.Span, error) + + // UnbindSpanFunc mocks the UnbindSpan method. + UnbindSpanFunc func(reconcileObject client.Object, phase string) error + + // calls tracks calls to the methods. + calls struct { + // GetSpan holds details about calls to the GetSpan method. + GetSpan []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Tracer is the tracer argument value. + Tracer trace.Tracer + // ReconcileObject is the reconcileObject argument value. + ReconcileObject client.Object + // Phase is the phase argument value. + Phase string + } + // UnbindSpan holds details about calls to the UnbindSpan method. + UnbindSpan []struct { + // ReconcileObject is the reconcileObject argument value. + ReconcileObject client.Object + // Phase is the phase argument value. + Phase string + } + } + lockGetSpan sync.RWMutex + lockUnbindSpan sync.RWMutex +} + +// GetSpan calls GetSpanFunc. +func (mock *SpanHandlerIMock) GetSpan(ctx context.Context, tracer trace.Tracer, reconcileObject client.Object, phase string) (context.Context, trace.Span, error) { + if mock.GetSpanFunc == nil { + panic("SpanHandlerIMock.GetSpanFunc: method is nil but SpanHandlerI.GetSpan was just called") + } + callInfo := struct { + Ctx context.Context + Tracer trace.Tracer + ReconcileObject client.Object + Phase string + }{ + Ctx: ctx, + Tracer: tracer, + ReconcileObject: reconcileObject, + Phase: phase, + } + mock.lockGetSpan.Lock() + mock.calls.GetSpan = append(mock.calls.GetSpan, callInfo) + mock.lockGetSpan.Unlock() + return mock.GetSpanFunc(ctx, tracer, reconcileObject, phase) +} + +// GetSpanCalls gets all the calls that were made to GetSpan. +// Check the length with: +// +// len(mockedSpanHandlerI.GetSpanCalls()) +func (mock *SpanHandlerIMock) GetSpanCalls() []struct { + Ctx context.Context + Tracer trace.Tracer + ReconcileObject client.Object + Phase string +} { + var calls []struct { + Ctx context.Context + Tracer trace.Tracer + ReconcileObject client.Object + Phase string + } + mock.lockGetSpan.RLock() + calls = mock.calls.GetSpan + mock.lockGetSpan.RUnlock() + return calls +} + +// UnbindSpan calls UnbindSpanFunc. +func (mock *SpanHandlerIMock) UnbindSpan(reconcileObject client.Object, phase string) error { + if mock.UnbindSpanFunc == nil { + panic("SpanHandlerIMock.UnbindSpanFunc: method is nil but SpanHandlerI.UnbindSpan was just called") + } + callInfo := struct { + ReconcileObject client.Object + Phase string + }{ + ReconcileObject: reconcileObject, + Phase: phase, + } + mock.lockUnbindSpan.Lock() + mock.calls.UnbindSpan = append(mock.calls.UnbindSpan, callInfo) + mock.lockUnbindSpan.Unlock() + return mock.UnbindSpanFunc(reconcileObject, phase) +} + +// UnbindSpanCalls gets all the calls that were made to UnbindSpan. +// Check the length with: +// +// len(mockedSpanHandlerI.UnbindSpanCalls()) +func (mock *SpanHandlerIMock) UnbindSpanCalls() []struct { + ReconcileObject client.Object + Phase string +} { + var calls []struct { + ReconcileObject client.Object + Phase string + } + mock.lockUnbindSpan.RLock() + calls = mock.calls.UnbindSpan + mock.lockUnbindSpan.RUnlock() + return calls +} diff --git a/operator/controllers/common/phasehandler.go b/operator/controllers/common/phasehandler.go index b3eb0c7423..a59fc021c5 100644 --- a/operator/controllers/common/phasehandler.go +++ b/operator/controllers/common/phasehandler.go @@ -18,7 +18,7 @@ type PhaseHandler struct { client.Client Recorder record.EventRecorder Log logr.Logger - SpanHandler *SpanHandler + SpanHandler ISpanHandler } type PhaseResult struct { @@ -44,14 +44,14 @@ func (r PhaseHandler) HandlePhase(ctx context.Context, ctxTrace context.Context, piWrapper.SetCurrentPhase(phase.ShortName) r.Log.Info(phase.LongName + " not finished") - ctxTrace, spanTrace, err := r.SpanHandler.GetSpan(ctxTrace, tracer, reconcileObject, phase.ShortName) + _, spanAppTrace, err := r.SpanHandler.GetSpan(ctxTrace, tracer, reconcileObject, phase.ShortName) if err != nil { r.Log.Error(err, "could not get span") } state, err := reconcilePhase() if err != nil { - spanTrace.AddEvent(phase.LongName + " could not get reconciled") + spanAppTrace.AddEvent(phase.LongName + " could not get reconciled") RecordEvent(r.Recorder, phase, "Warning", reconcileObject, "ReconcileErrored", "could not get reconciled", piWrapper.GetVersion()) span.SetStatus(codes.Error, err.Error()) return &PhaseResult{Continue: false, Result: requeueResult}, err @@ -64,7 +64,7 @@ func (r PhaseHandler) HandlePhase(ctx context.Context, ctxTrace context.Context, defer func(oldStatus common.KeptnState, oldPhase string, reconcileObject client.Object) { piWrapper, _ := NewPhaseItemWrapperFromClientObject(reconcileObject) if oldStatus != piWrapper.GetState() || oldPhase != piWrapper.GetCurrentPhase() { - ctx, spanTrace, err = r.SpanHandler.GetSpan(ctxTrace, tracer, reconcileObject, piWrapper.GetCurrentPhase()) + ctx, spanAppTrace, err = r.SpanHandler.GetSpan(ctxTrace, tracer, reconcileObject, piWrapper.GetCurrentPhase()) if err != nil { r.Log.Error(err, "could not get span") } @@ -78,9 +78,9 @@ func (r PhaseHandler) HandlePhase(ctx context.Context, ctxTrace context.Context, if state.IsFailed() { piWrapper.Complete() piWrapper.SetState(common.StateFailed) - spanTrace.AddEvent(phase.LongName + " has failed") - spanTrace.SetStatus(codes.Error, "Failed") - spanTrace.End() + spanAppTrace.AddEvent(phase.LongName + " has failed") + spanAppTrace.SetStatus(codes.Error, "Failed") + spanAppTrace.End() if err := r.SpanHandler.UnbindSpan(reconcileObject, phase.ShortName); err != nil { r.Log.Error(err, "cannot unbind span") } @@ -90,9 +90,9 @@ func (r PhaseHandler) HandlePhase(ctx context.Context, ctxTrace context.Context, } piWrapper.SetState(common.StateSucceeded) - spanTrace.AddEvent(phase.LongName + " has succeeded") - spanTrace.SetStatus(codes.Ok, "Succeeded") - spanTrace.End() + spanAppTrace.AddEvent(phase.LongName + " has succeeded") + spanAppTrace.SetStatus(codes.Ok, "Succeeded") + spanAppTrace.End() if err := r.SpanHandler.UnbindSpan(reconcileObject, phase.ShortName); err != nil { r.Log.Error(err, "cannot unbind span") } diff --git a/operator/controllers/common/spanhandler.go b/operator/controllers/common/spanhandler.go index a5fc7c4e65..e61fbf3d9e 100644 --- a/operator/controllers/common/spanhandler.go +++ b/operator/controllers/common/spanhandler.go @@ -8,6 +8,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) +//go:generate moq -pkg fake -skip-ensure -out ./fake/spanhandler_mock.go . ISpanHandler +type ISpanHandler interface { + GetSpan(ctx context.Context, tracer trace.Tracer, reconcileObject client.Object, phase string) (context.Context, trace.Span, error) + UnbindSpan(reconcileObject client.Object, phase string) error +} + type SpanHandler struct { bindCRDSpan map[string]trace.Span mtx sync.Mutex diff --git a/operator/controllers/common/test_utils.go b/operator/controllers/common/test_utils.go new file mode 100644 index 0000000000..c529556ba1 --- /dev/null +++ b/operator/controllers/common/test_utils.go @@ -0,0 +1,38 @@ +package common + +import ( + "context" + lfcv1alpha1 "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func AddApp(c client.Client, name string) error { + app := &lfcv1alpha1.KeptnApp{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: lfcv1alpha1.KeptnAppSpec{ + Version: "1.0.0", + }, + Status: lfcv1alpha1.KeptnAppStatus{}, + } + return c.Create(context.TODO(), app) + +} + +func AddAppVersion(c client.Client, name string, status lfcv1alpha1.KeptnAppVersionStatus) error { + app := &lfcv1alpha1.KeptnAppVersion{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: lfcv1alpha1.KeptnAppVersionSpec{KeptnAppSpec: lfcv1alpha1.KeptnAppSpec{Version: "1.0.0"}}, + Status: status, + } + return c.Create(context.TODO(), app) + +} diff --git a/operator/controllers/keptnapp/controller_test.go b/operator/controllers/keptnapp/controller_test.go index c84dc2ed21..30a2745b99 100644 --- a/operator/controllers/keptnapp/controller_test.go +++ b/operator/controllers/keptnapp/controller_test.go @@ -4,6 +4,7 @@ import ( "context" lfcv1alpha1 "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" keptncommon "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1/common" + utils "github.com/keptn/lifecycle-toolkit/operator/controllers/common" "github.com/keptn/lifecycle-toolkit/operator/controllers/common/fake" "github.com/magiconair/properties/assert" "go.opentelemetry.io/otel/trace" @@ -43,7 +44,7 @@ func TestKeptnAppReconciler_createAppVersionSuccess(t *testing.T) { } -func TestKeptnAppReconciler_Reconcile(t *testing.T) { +func TestKeptnAppReconciler_reconcile(t *testing.T) { r, eventChannel, tracer := setupReconciler(t) @@ -88,9 +89,9 @@ func TestKeptnAppReconciler_Reconcile(t *testing.T) { //setting up fakeclient CRD data - addApp(r, "myapp") - addApp(r, "myfinishedapp") - addAppVersion(r, "myfinishedapp-1.0.0", keptncommon.StateSucceeded) + utils.AddApp(r.Client, "myapp") + utils.AddApp(r.Client, "myfinishedapp") + utils.AddAppVersion(r.Client, "myfinishedapp-1.0.0", lfcv1alpha1.KeptnAppVersionStatus{Status: keptncommon.StateSucceeded}) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -146,35 +147,3 @@ func setupReconciler(t *testing.T) (*KeptnAppReconciler, chan string, *fake.ITra } return r, recorder.Events, tr } - -func addApp(r *KeptnAppReconciler, name string) error { - app := &lfcv1alpha1.KeptnApp{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - Spec: lfcv1alpha1.KeptnAppSpec{ - Version: "1.0.0", - }, - Status: lfcv1alpha1.KeptnAppStatus{}, - } - return r.Client.Create(context.TODO(), app) - -} - -func addAppVersion(r *KeptnAppReconciler, name string, status keptncommon.KeptnState) error { - app := &lfcv1alpha1.KeptnAppVersion{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - Spec: lfcv1alpha1.KeptnAppVersionSpec{}, - Status: lfcv1alpha1.KeptnAppVersionStatus{ - Status: status, - }, - } - return r.Client.Create(context.TODO(), app) - -} diff --git a/operator/controllers/keptnappversion/controller.go b/operator/controllers/keptnappversion/controller.go index 4435f0e835..69c151be4d 100644 --- a/operator/controllers/keptnappversion/controller.go +++ b/operator/controllers/keptnappversion/controller.go @@ -48,9 +48,9 @@ type KeptnAppVersionReconciler struct { client.Client Log logr.Logger Recorder record.EventRecorder - Tracer trace.Tracer + Tracer controllercommon.ITracer Meters common.KeptnMeters - SpanHandler *controllercommon.SpanHandler + SpanHandler controllercommon.ISpanHandler } //+kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptnappversions,verbs=get;list;watch;create;update;patch;delete @@ -82,26 +82,9 @@ func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Requ return reconcile.Result{}, fmt.Errorf(controllercommon.ErrCannotFetchAppVersionMsg, err) } - appVersion.SetStartTime() - - traceContextCarrier := propagation.MapCarrier(appVersion.Annotations) - ctx = otel.GetTextMapPropagator().Extract(ctx, traceContextCarrier) - - appTraceContextCarrier := propagation.MapCarrier(appVersion.Spec.TraceId) - ctxAppTrace := otel.GetTextMapPropagator().Extract(context.TODO(), appTraceContextCarrier) + ctx, ctxAppTrace, span, endSpan := setupSpansContexts(ctx, appVersion, r) - ctx, span := r.Tracer.Start(ctx, "reconcile_app_version", trace.WithSpanKind(trace.SpanKindConsumer)) - - defer func(span trace.Span, appVersion *klcv1alpha1.KeptnAppVersion) { - if appVersion.IsEndTimeSet() { - r.Log.Info("Increasing app count") - attrs := appVersion.GetMetricsAttributes() - r.Meters.AppCount.Add(ctx, 1, attrs...) - } - span.End() - }(span, appVersion) - - appVersion.SetSpanAttributes(span) + defer endSpan() phase := common.PhaseAppPreDeployment @@ -117,7 +100,8 @@ func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Requ r.Log.Error(err, "cannot unbind span") } var spanAppTrace trace.Span - ctxAppTrace, spanAppTrace, err = r.SpanHandler.GetSpan(ctxAppTrace, r.Tracer, appVersion, phase.ShortName) + // ignoring ctx to don't use this as parent span + _, spanAppTrace, err = r.SpanHandler.GetSpan(ctxAppTrace, r.Tracer, appVersion, phase.ShortName) if err != nil { r.Log.Error(err, "could not get span") } @@ -209,6 +193,30 @@ func (r *KeptnAppVersionReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, nil } +func setupSpansContexts(ctx context.Context, appVersion *klcv1alpha1.KeptnAppVersion, r *KeptnAppVersionReconciler) (context.Context, context.Context, trace.Span, func()) { + appVersion.SetStartTime() + + traceContextCarrier := propagation.MapCarrier(appVersion.Annotations) + ctx = otel.GetTextMapPropagator().Extract(ctx, traceContextCarrier) + + appTraceContextCarrier := propagation.MapCarrier(appVersion.Spec.TraceId) + ctxAppTrace := otel.GetTextMapPropagator().Extract(context.TODO(), appTraceContextCarrier) + + ctx, span := r.Tracer.Start(ctx, "reconcile_app_version", trace.WithSpanKind(trace.SpanKindConsumer)) + + endFunc := func() { + if appVersion.IsEndTimeSet() { + r.Log.Info("Increasing app count") + attrs := appVersion.GetMetricsAttributes() + r.Meters.AppCount.Add(ctx, 1, attrs...) + } + span.End() + } + + appVersion.SetSpanAttributes(span) + return ctx, ctxAppTrace, span, endFunc +} + // SetupWithManager sets up the controller with the Manager. func (r *KeptnAppVersionReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/operator/controllers/keptnappversion/controller_test.go b/operator/controllers/keptnappversion/controller_test.go new file mode 100644 index 0000000000..f01f929e0c --- /dev/null +++ b/operator/controllers/keptnappversion/controller_test.go @@ -0,0 +1,219 @@ +package keptnappversion + +import ( + "context" + "fmt" + lfcv1alpha1 "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" + keptncommon "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1/common" + utils "github.com/keptn/lifecycle-toolkit/operator/controllers/common" + "github.com/keptn/lifecycle-toolkit/operator/controllers/common/fake" + "github.com/magiconair/properties/assert" + "go.opentelemetry.io/otel/metric/instrument" + "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/trace" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "reflect" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "strings" + "testing" +) + +// this test checks if the chain of reconcile events is correct +func TestKeptnAppVersionReconciler_reconcile(t *testing.T) { + + r, eventChannel, tracer, _ := setupReconciler(t) + + tests := []struct { + name string + req ctrl.Request + wantErr error + events []string //check correct events are generated + startTrace bool + }{ + { + name: "new appVersion with no workload nor evaluation should finish", + req: ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "default", + Name: "myappversion-1.0.0", + }, + }, + wantErr: nil, + events: []string{ + `AppPreDeployTasksStarted`, + `AppPreDeployTasksSucceeded`, + `AppPreDeployEvaluationsSucceeded`, + `AppDeploySucceeded`, + `AppPostDeployTasksSucceeded`, + `AppPostDeployEvaluationsSucceeded`, + `AppPostDeployEvaluationsFinished`, + }, + startTrace: true, + }, + { + name: "notfound should not return error nor event", + req: ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "default", + Name: "mynotthereapp", + }, + }, + wantErr: nil, + }, + { + name: "existing appVersion has finished", + req: ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "default", + Name: "myfinishedapp-1.0.0", + }, + }, + wantErr: nil, + events: []string{`AppPostDeployEvaluationsFinished`}, + startTrace: true, + }, + } + + //setting up fakeclient CRD data + + utils.AddAppVersion(r.Client, "myappversion-1.0.0", lfcv1alpha1.KeptnAppVersionStatus{Status: keptncommon.StatePending}) + utils.AddAppVersion(r.Client, "myfinishedapp-1.0.0", createFinishedAppVersionStatus()) + + traces := 0 + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + _, err := r.Reconcile(context.WithValue(context.TODO(), "start", tt.req.Name), tt.req) + if !reflect.DeepEqual(err, tt.wantErr) { + t.Errorf("Reconcile() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.events != nil { + for _, e := range tt.events { + event := <-eventChannel + assert.Equal(t, strings.Contains(event, tt.req.Name), true, "wrong appversion") + assert.Equal(t, strings.Contains(event, tt.req.Namespace), true, "wrong namespace") + assert.Equal(t, strings.Contains(event, e), true, fmt.Sprintf("no %s found in %s", e, event)) + } + + } + if tt.startTrace { + //A different trace for each app-version + assert.Equal(t, tracer.StartCalls()[traces].SpanName, "reconcile_app_version") + assert.Equal(t, tracer.StartCalls()[traces].Ctx.Value("start"), tt.req.Name) + traces++ + } + }) + + } + +} + +func createFinishedAppVersionStatus() lfcv1alpha1.KeptnAppVersionStatus { + return lfcv1alpha1.KeptnAppVersionStatus{ + CurrentPhase: keptncommon.PhaseCompleted.ShortName, + PreDeploymentStatus: keptncommon.StateSucceeded, + PostDeploymentStatus: keptncommon.StateSucceeded, + PreDeploymentEvaluationStatus: keptncommon.StateSucceeded, + PostDeploymentEvaluationStatus: keptncommon.StateSucceeded, + PreDeploymentTaskStatus: []lfcv1alpha1.TaskStatus{{Status: keptncommon.StateSucceeded}}, + PostDeploymentTaskStatus: []lfcv1alpha1.TaskStatus{{Status: keptncommon.StateSucceeded}}, + PreDeploymentEvaluationTaskStatus: []lfcv1alpha1.EvaluationStatus{{Status: keptncommon.StateSucceeded}}, + PostDeploymentEvaluationTaskStatus: []lfcv1alpha1.EvaluationStatus{{Status: keptncommon.StateSucceeded}}, + WorkloadOverallStatus: keptncommon.StateSucceeded, + WorkloadStatus: []lfcv1alpha1.WorkloadStatus{{Status: keptncommon.StateSucceeded}}, + Status: keptncommon.StateSucceeded, + } +} + +func setupReconciler(t *testing.T) (*KeptnAppVersionReconciler, chan string, *fake.ITracerMock, *fake.SpanHandlerIMock) { + //setup logger + opts := zap.Options{ + Development: true, + } + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + //fake a tracer + tr := &fake.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return ctx, trace.SpanFromContext(ctx) + }} + + //fake span handler + + spanRecorder := &fake.SpanHandlerIMock{ + GetSpanFunc: func(ctx context.Context, tracer trace.Tracer, reconcileObject client.Object, phase string) (context.Context, trace.Span, error) { + return ctx, trace.SpanFromContext(ctx), nil + }, + UnbindSpanFunc: func(reconcileObject client.Object, phase string) error { return nil }, + } + + fakeClient, err := fake.NewClient() + if err != nil { + t.Errorf("Reconcile() error when setting up fake client %v", err) + } + recorder := record.NewFakeRecorder(100) + r := &KeptnAppVersionReconciler{ + Client: fakeClient, + Scheme: scheme.Scheme, + Recorder: recorder, + Log: ctrl.Log.WithName("test-appVersionController"), + Tracer: tr, + SpanHandler: spanRecorder, + Meters: InitAppMeters(), + } + return r, recorder.Events, tr, spanRecorder +} + +func TestKeptnApVersionReconciler_setupSpansContexts(t *testing.T) { + + r, _, _, _ := setupReconciler(t) + type args struct { + ctx context.Context + appVersion *lfcv1alpha1.KeptnAppVersion + } + tests := []struct { + name string + args args + baseCtx context.Context + appCtx context.Context + }{ + {name: "Current trace ctx should be != than app trace context", + args: args{ + ctx: context.WithValue(context.TODO(), "start", 1), + appVersion: &lfcv1alpha1.KeptnAppVersion{}, + }, + baseCtx: context.WithValue(context.TODO(), "start", 1), + appCtx: context.TODO(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, ctxAppTrace, _, _ := setupSpansContexts(tt.args.ctx, tt.args.appVersion, r) + if !reflect.DeepEqual(ctx, tt.baseCtx) { + t.Errorf("setupSpansContexts() got: %v as baseCtx, wanted: %v", ctx, tt.baseCtx) + } + if !reflect.DeepEqual(ctxAppTrace, tt.appCtx) { + t.Errorf("setupSpansContexts() got: %v as appCtx, wanted: %v", ctxAppTrace, tt.appCtx) + } + + }) + } +} + +func InitAppMeters() keptncommon.KeptnMeters { + provider := metric.NewMeterProvider() + meter := provider.Meter("keptn/task") + appCount, _ := meter.SyncInt64().Counter("keptn.app.count", instrument.WithDescription("a simple counter for Keptn Apps")) + appDuration, _ := meter.SyncFloat64().Histogram("keptn.app.duration", instrument.WithDescription("a histogram of duration for Keptn Apps"), instrument.WithUnit("s")) + + meters := keptncommon.KeptnMeters{ + AppCount: appCount, + AppDuration: appDuration, + } + return meters +} diff --git a/operator/test/component/appcontroller_test.go b/operator/test/component/appcontroller_test.go index bfc1207543..cab1e14d59 100644 --- a/operator/test/component/appcontroller_test.go +++ b/operator/test/component/appcontroller_test.go @@ -1,6 +1,7 @@ package component import ( + "fmt" klcv1alpha1 "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1/common" keptncontroller "github.com/keptn/lifecycle-toolkit/operator/controllers/common" @@ -11,6 +12,7 @@ import ( sdktest "go.opentelemetry.io/otel/sdk/trace/tracetest" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/storage/names" ) // clean example of component test (E2E test/ integration test can be achieved adding a real cluster) @@ -63,7 +65,7 @@ var _ = Describe("KeptnAppController", Ordered, func() { }) BeforeEach(func() { // list var here they will be copied for every spec - name = "test-appcontroller" + name = names.SimpleNameGenerator.GenerateName("my-app-") namespace = "default" // namespaces are not deleted in the api so be careful // when creating you can use ignoreAlreadyExists(err error) version = "1.0.0" @@ -75,15 +77,16 @@ var _ = Describe("KeptnAppController", Ordered, func() { BeforeEach(func() { instance = createInstanceInCluster(name, namespace, version, instance) + fmt.Println("created ", instance.Name) }) Context("with a new App CRD", func() { - It("should update the status of the CR ", func() { - assertResourceUpdated(instance) - }) It("should update the spans", func() { + By("creating a new app version") + assertResourceUpdated(instance) assertAppSpan(instance, spanRecorder) + fmt.Println("spanned ", instance.Name) }) }) @@ -104,21 +107,28 @@ func deleteAppInCluster(instance *klcv1alpha1.KeptnApp) { func assertResourceUpdated(instance *klcv1alpha1.KeptnApp) *klcv1alpha1.KeptnAppVersion { - appVersion := &klcv1alpha1.KeptnAppVersion{} + appVersion := getAppVersion(instance) + + By("Comparing expected app version") + Expect(appVersion.Spec.AppName).To(Equal(instance.Name)) + Expect(appVersion.Spec.Version).To(Equal(instance.Spec.Version)) + Expect(appVersion.Spec.Workloads).To(Equal(instance.Spec.Workloads)) + + return appVersion +} + +func getAppVersion(instance *klcv1alpha1.KeptnApp) *klcv1alpha1.KeptnAppVersion { appvName := types.NamespacedName{ Namespace: instance.Namespace, Name: instance.Name + "-" + instance.Spec.Version, } + + appVersion := &klcv1alpha1.KeptnAppVersion{} By("Retrieving Created app version") Eventually(func() error { return k8sClient.Get(ctx, appvName, appVersion) }, "20s").Should(Succeed()) - By("Comparing expected app version") - Expect(appVersion.Spec.AppName).To(Equal(instance.Name)) - Expect(appVersion.Spec.Version).To(Equal(instance.Spec.Version)) - Expect(appVersion.Spec.Workloads).To(Equal(instance.Spec.Workloads)) - return appVersion } @@ -161,6 +171,6 @@ func createInstanceInCluster(name string, namespace string, version string, inst } By("Invoking Reconciling for Create") - Expect(ignoreAlreadyExists(k8sClient.Create(ctx, instance))).Should(Succeed()) + Expect(k8sClient.Create(ctx, instance)).Should(Succeed()) return instance }