Skip to content

Commit

Permalink
chore(lifecycle-operator): refactor pod mutating webhook (#2233)
Browse files Browse the repository at this point in the history
Signed-off-by: realanna <anna.reale@dynatrace.com>
Signed-off-by: RealAnna <89971034+RealAnna@users.noreply.github.com>
Co-authored-by: odubajDT <93584209+odubajDT@users.noreply.github.com>
  • Loading branch information
RealAnna and odubajDT committed Oct 11, 2023
1 parent d95dc10 commit c2cc89a
Show file tree
Hide file tree
Showing 14 changed files with 2,704 additions and 1,606 deletions.
15 changes: 7 additions & 8 deletions lifecycle-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,14 +383,13 @@ func main() {
webhookRecorder := mgr.GetEventRecorderFor("keptn/webhook")
webhookBuilder.Register(mgr, map[string]*ctrlWebhook.Admission{
"/mutate-v1-pod": {
Handler: &pod_mutator.PodMutatingWebhook{
SchedulingGatesEnabled: env.SchedulingGatesEnabled,
Client: mgr.GetClient(),
Tracer: otel.Tracer("keptn/webhook"),
EventSender: controllercommon.NewEventMultiplexer(webhookLogger, webhookRecorder, ceClient),
Decoder: admission.NewDecoder(mgr.GetScheme()),
Log: webhookLogger,
},
Handler: pod_mutator.NewPodMutator(
mgr.GetClient(),
otel.Tracer("keptn/webhook"),
admission.NewDecoder(mgr.GetScheme()),
controllercommon.NewEventMultiplexer(webhookLogger, webhookRecorder, ceClient),
webhookLogger,
env.SchedulingGatesEnabled),
},
})
setupLog.Info("starting webhook")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package handlers

import (
"context"
"fmt"

"github.com/go-logr/logr"
klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3"
apicommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3/common"
controllercommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/common"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type AppCreationRequestHandler struct {
Client client.Client
Log logr.Logger
Tracer trace.Tracer
EventSender controllercommon.IEvent
}

func (a *AppCreationRequestHandler) Handle(ctx context.Context, pod *corev1.Pod, namespace string) error {

ctx, span := a.Tracer.Start(ctx, "create_appCreationRequest", trace.WithSpanKind(trace.SpanKindProducer))
defer span.End()

newAppCreationRequest := generateResource(ctx, pod, namespace)
newAppCreationRequest.SetSpanAttributes(span)

a.Log.Info("Searching for AppCreationRequest", "appCreationRequest", newAppCreationRequest.Name, "namespace", newAppCreationRequest.Namespace)

appCreationRequest := &klcv1alpha3.KeptnAppCreationRequest{}
err := a.Client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: newAppCreationRequest.Name}, appCreationRequest)
if errors.IsNotFound(err) {
return a.createResource(ctx, newAppCreationRequest, span)
}

if err != nil {
span.SetStatus(codes.Error, err.Error())

return fmt.Errorf("could not fetch AppCreationRequest %w", err)
}
a.Log.Info("Found AppCreationRequest", "appCreationRequest", newAppCreationRequest.Name, "namespace", newAppCreationRequest.Namespace)
return nil
}

func (a *AppCreationRequestHandler) createResource(ctx context.Context, newAppCreationRequest *klcv1alpha3.KeptnAppCreationRequest, span trace.Span) error {
a.Log.Info("Creating app creation request", "appCreationRequest", newAppCreationRequest.Name, "namespace", newAppCreationRequest.Namespace)

err := a.Client.Create(ctx, newAppCreationRequest)
if err != nil {
a.Log.Error(err, "Could not create AppCreationRequest")
a.EventSender.Emit(apicommon.PhaseCreateAppCreationRequest, "Warning", newAppCreationRequest, apicommon.PhaseStateFailed, "could not create KeptnAppCreationRequest", newAppCreationRequest.Spec.AppName)
span.SetStatus(codes.Error, err.Error())
return err
}

return nil
}

func generateResource(ctx context.Context, pod *corev1.Pod, namespace string) *klcv1alpha3.KeptnAppCreationRequest {

// create TraceContext
// follow up with a Keptn propagator that JSON-encoded the OTel map into our own key
traceContextCarrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, traceContextCarrier)

kacr := &klcv1alpha3.KeptnAppCreationRequest{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Annotations: traceContextCarrier,
},
}

if !isAppAnnotationPresent(&pod.ObjectMeta) {
initEmptyAnnotations(&pod.ObjectMeta, 2)
// at this point if the pod does not have an app annotation it means we create the app
// and it will have a single workload
pod.ObjectMeta.Annotations[apicommon.AppAnnotation] = pod.ObjectMeta.Annotations[apicommon.WorkloadAnnotation]
// so we can mark the app request as single service type
kacr.Annotations[apicommon.AppTypeAnnotation] = string(apicommon.AppTypeSingleService)
}

appName := getAppName(&pod.ObjectMeta)
kacr.ObjectMeta.Name = appName
kacr.Spec = klcv1alpha3.KeptnAppCreationRequestSpec{
AppName: appName,
}

return kacr
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
package handlers

import (
"context"
"errors"
"testing"

"github.com/go-logr/logr"
"github.com/go-logr/logr/testr"
klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3"
apicommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3/common"
"github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/common"
"github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/common/fake"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
corev1 "k8s.io/api/core/v1"
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"
k8sfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
)

const testApp = "my-app"
const TestWorkload = "my-workload"

var errCreate = errors.New("badcreate")
var errAppCreate = errors.New("bad")

func TestAppHandlerHandle(t *testing.T) {

mockEventSender := common.NewK8sSender(record.NewFakeRecorder(100))
log := testr.New(t)
tr := &fake.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return ctx, trace.SpanFromContext(ctx)
}}

pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "example-pod",
Namespace: namespace,
Annotations: map[string]string{
apicommon.WorkloadAnnotation: TestWorkload,
apicommon.VersionAnnotation: "0.1",
},
}}

singleServiceCreationReq := &klcv1alpha3.KeptnAppCreationRequest{
TypeMeta: metav1.TypeMeta{
Kind: "KeptnAppCreationRequest",
APIVersion: "lifecycle.keptn.sh/v1alpha3",
},
ObjectMeta: metav1.ObjectMeta{
Name: TestWorkload,
Namespace: namespace,
ResourceVersion: "1",
Annotations: map[string]string{
"keptn.sh/app-type": "single-service",
},
},
Spec: klcv1alpha3.KeptnAppCreationRequestSpec{AppName: TestWorkload},
}

tests := []struct {
name string
client client.Client
pod *corev1.Pod
wanterr error
wantReq *klcv1alpha3.KeptnAppCreationRequest
}{
{
name: "Create AppCreationRequest inherit from workload",
pod: pod,
client: fake.NewClient(),
wantReq: singleServiceCreationReq,
},
{
name: "AppCreationRequest already exists",
pod: pod,
client: fake.NewClient(singleServiceCreationReq),
wantReq: singleServiceCreationReq,
},
{
name: "Create AppCreationRequest",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "example-pod",
Namespace: namespace,
Annotations: map[string]string{
apicommon.AppAnnotation: testApp,
apicommon.WorkloadAnnotation: TestWorkload,
apicommon.VersionAnnotation: "0.1",
},
}},
client: fake.NewClient(),
wantReq: &klcv1alpha3.KeptnAppCreationRequest{
TypeMeta: metav1.TypeMeta{
Kind: "KeptnAppCreationRequest",
APIVersion: "lifecycle.keptn.sh/v1alpha3",
},
ObjectMeta: metav1.ObjectMeta{
Name: testApp,
Namespace: namespace,
ResourceVersion: "1",
},
Spec: klcv1alpha3.KeptnAppCreationRequestSpec{AppName: testApp},
},
},
{
name: "Error Fetching AppCreationRequest",
pod: &corev1.Pod{},
client: k8sfake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
Get: func(ctx context.Context, client client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
return errAppCreate
},
}).Build(),
wanterr: errAppCreate,
},
{
name: "Error Creating AppCreationRequest",
pod: pod,
client: k8sfake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
Create: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
return errCreate
},
}).Build(),
wanterr: errCreate,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
appHandler := &AppCreationRequestHandler{
Client: tt.client,
Log: log,
EventSender: mockEventSender,
Tracer: tr,
}
err := appHandler.Handle(context.TODO(), tt.pod, namespace)

if tt.wanterr != nil {
require.NotNil(t, err)
require.ErrorIs(t, err, tt.wanterr)
} else {
require.Nil(t, err)
}

if tt.wantReq != nil {
creationReq := &klcv1alpha3.KeptnAppCreationRequest{}
err = tt.client.Get(context.TODO(), types.NamespacedName{Name: tt.wantReq.Name, Namespace: tt.wantReq.Namespace}, creationReq)
require.Nil(t, err)
require.Equal(t, tt.wantReq, creationReq)
}

})
}
}

func TestAppHandlerCreateAppSucceeds(t *testing.T) {
fakeClient := fake.NewClient()
logger := logr.Discard()
eventSender := common.NewK8sSender(record.NewFakeRecorder(100))
tracer := &fake.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return ctx, trace.SpanFromContext(ctx)
}}
appHandler := &AppCreationRequestHandler{
Client: fakeClient,
Log: logger,
Tracer: tracer,
EventSender: eventSender,
}

ctx := context.TODO()
name := "myappcreationreq"
newAppCreationRequest := &klcv1alpha3.KeptnAppCreationRequest{
ObjectMeta: metav1.ObjectMeta{Name: name},
}
err := appHandler.createResource(ctx, newAppCreationRequest, trace.SpanFromContext(ctx))

require.Nil(t, err)
creationReq := &klcv1alpha3.KeptnAppCreationRequest{}
err = fakeClient.Get(ctx, types.NamespacedName{Name: name}, creationReq)
require.Nil(t, err)

}

func TestAppHandlerCreateAppFails(t *testing.T) {
fakeClient := fake.NewClient()
logger := logr.Discard()
eventSender := common.NewK8sSender(record.NewFakeRecorder(100))
tracer := &fake.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
return ctx, trace.SpanFromContext(ctx)
}}
appHandler := &AppCreationRequestHandler{
Client: fakeClient,
Log: logger,
Tracer: tracer,
EventSender: eventSender,
}

ctx := context.TODO()
newAppCreationRequest := &klcv1alpha3.KeptnAppCreationRequest{
ObjectMeta: metav1.ObjectMeta{},
}
err := appHandler.createResource(ctx, newAppCreationRequest, trace.SpanFromContext(ctx))
require.Error(t, err)

}

func TestGenerateAppCreationRequest(t *testing.T) {
// Mock a context with OpenTelemetry tracer enabled
ctx := context.Background()

// Create a sample Pod
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: namespace,
Annotations: map[string]string{
apicommon.WorkloadAnnotation: workloadName,
},
},
}

// Test case 1: Pod does not have app annotation
t.Run("PodWithoutAppAnnotation", func(t *testing.T) {
kacr := generateResource(ctx, pod, namespace)

require.Equal(t, namespace, kacr.Namespace)
require.Equal(t, string(apicommon.AppTypeSingleService), kacr.Annotations[apicommon.AppTypeAnnotation])
require.Equal(t, workloadName, kacr.Name)
require.Equal(t, workloadName, kacr.Spec.AppName)
})

// Test case 2: Pod has app annotation
t.Run("PodWithAppAnnotation", func(t *testing.T) {
// Add app annotation to the Pod
pod.ObjectMeta.Annotations[apicommon.AppAnnotation] = lowerAppName
kacr := generateResource(ctx, pod, namespace)

require.Equal(t, namespace, kacr.Namespace)
require.Empty(t, kacr.Annotations[apicommon.AppTypeAnnotation]) // No app type annotation
require.Equal(t, lowerAppName, kacr.Name)
require.Equal(t, lowerAppName, kacr.Spec.AppName)
})
}
Loading

0 comments on commit c2cc89a

Please sign in to comment.