-
Notifications
You must be signed in to change notification settings - Fork 118
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(lifecycle-operator): refactor pod mutating webhook (#2233)
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
Showing
14 changed files
with
2,704 additions
and
1,606 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
98 changes: 98 additions & 0 deletions
98
lifecycle-operator/webhooks/pod_mutator/handlers/appcreationrequest_handler.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
247 changes: 247 additions & 0 deletions
247
lifecycle-operator/webhooks/pod_mutator/handlers/appcreationrequest_handler_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
Oops, something went wrong.