Skip to content

Commit

Permalink
fix: fix exception handling in AsyncDefaultAdmissionRequestMutator an…
Browse files Browse the repository at this point in the history
…d add constructor for Mutator interface

Signed-off-by: David Sondermann <[email protected]>
  • Loading branch information
Donnerbart committed Nov 19, 2024
1 parent 6ca8623 commit d530946
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,22 @@
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
import io.javaoperatorsdk.webhook.admission.mutation.AsyncDefaultAdmissionRequestMutator;
import io.javaoperatorsdk.webhook.admission.mutation.AsyncMutator;
import io.javaoperatorsdk.webhook.admission.mutation.Mutator;
import io.javaoperatorsdk.webhook.admission.validation.AsyncDefaultAdmissionRequestValidator;
import io.javaoperatorsdk.webhook.admission.validation.Validator;

public class AsyncAdmissionController<T extends KubernetesResource> {

private final AsyncAdmissionRequestHandler requestHandler;

public AsyncAdmissionController(AsyncMutator<T> mutator) {
public AsyncAdmissionController(Mutator<T> mutator) {
this(new AsyncDefaultAdmissionRequestMutator<>(mutator));
}

public AsyncAdmissionController(AsyncMutator<T> asyncMutator) {
this(new AsyncDefaultAdmissionRequestMutator<>(asyncMutator));
}

public AsyncAdmissionController(Validator<T> validator) {
this(new AsyncDefaultAdmissionRequestValidator<>(validator));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.javaoperatorsdk.webhook.admission.mutation;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;

import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionResponse;
import io.javaoperatorsdk.webhook.admission.AdmissionUtils;
import io.javaoperatorsdk.webhook.admission.AsyncAdmissionRequestHandler;
import io.javaoperatorsdk.webhook.admission.NotAllowedException;
import io.javaoperatorsdk.webhook.admission.Operation;
Expand All @@ -15,19 +15,29 @@

import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.admissionResponseFromMutation;
import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.getTargetResource;
import static io.javaoperatorsdk.webhook.admission.AdmissionUtils.notAllowedExceptionToAdmissionResponse;

public class AsyncDefaultAdmissionRequestMutator<T extends KubernetesResource>
implements AsyncAdmissionRequestHandler {

private final AsyncMutator<T> mutator;
private final AsyncMutator<T> asyncMutator;
private final Cloner<T> cloner;

public AsyncDefaultAdmissionRequestMutator(AsyncMutator<T> mutator) {
public AsyncDefaultAdmissionRequestMutator(Mutator<T> mutator) {
this(mutator, new ObjectMapperCloner<>());
}

public AsyncDefaultAdmissionRequestMutator(AsyncMutator<T> mutator, Cloner<T> cloner) {
this.mutator = mutator;
public AsyncDefaultAdmissionRequestMutator(Mutator<T> mutator, Cloner<T> cloner) {
this((AsyncMutator<T>) (resource, operation) -> CompletableFuture.supplyAsync(
() -> mutator.mutate(resource, operation)), cloner);
}

public AsyncDefaultAdmissionRequestMutator(AsyncMutator<T> asyncMutator) {
this(asyncMutator, new ObjectMapperCloner<>());
}

public AsyncDefaultAdmissionRequestMutator(AsyncMutator<T> asyncMutator, Cloner<T> cloner) {
this.asyncMutator = asyncMutator;
this.cloner = cloner;
}

Expand All @@ -37,15 +47,16 @@ public CompletionStage<AdmissionResponse> handle(AdmissionRequest admissionReque
var operation = Operation.valueOf(admissionRequest.getOperation());
var originalResource = (T) getTargetResource(admissionRequest, operation);
var clonedResource = cloner.clone(originalResource);
CompletionStage<AdmissionResponse> admissionResponse;
try {
var mutatedResource = mutator.mutate(clonedResource, operation);
admissionResponse =
mutatedResource.thenApply(mr -> admissionResponseFromMutation(originalResource, mr));
} catch (NotAllowedException e) {
admissionResponse = CompletableFuture
.supplyAsync(() -> AdmissionUtils.notAllowedExceptionToAdmissionResponse(e));
}
return admissionResponse;
return asyncMutator.mutate(clonedResource, operation)
.thenApply(resource -> admissionResponseFromMutation(originalResource, resource))
.exceptionally(e -> {
if (e instanceof CompletionException) {
if (e.getCause() instanceof NotAllowedException) {
return notAllowedExceptionToAdmissionResponse((NotAllowedException) e.getCause());
}
throw new IllegalStateException(e.getCause());
}
throw new IllegalStateException(e);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,74 @@
import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.webhook.admission.mutation.Mutator;
import io.javaoperatorsdk.webhook.admission.validation.Validator;

import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.*;
import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.LABEL_TEST_VALUE;
import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.MISSING_REQUIRED_LABEL;

class AdmissionControllerTest {

AdmissionTestSupport admissionTestSupport = new AdmissionTestSupport();

@Test
void validatesResource() {
AdmissionController<HasMetadata> admissionController =
new AdmissionController<>((resource, operation) -> {
if (resource.getMetadata().getLabels().get(AdmissionTestSupport.LABEL_KEY) == null) {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
}
});
var admissionController = new AdmissionController<HasMetadata>((resource, operation) -> {
});
admissionTestSupport.validatesResource(admissionController::handle);
}

@Test
void validatesResource_whenNotAllowedException() {
var admissionController =
new AdmissionController<>((Validator<HasMetadata>) (resource, operation) -> {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
});
admissionTestSupport.notAllowedException(admissionController::handle);
}

@Test
void validatesResource_whenOtherException() {
var admissionController =
new AdmissionController<>((Validator<HasMetadata>) (resource, operation) -> {
throw new IllegalArgumentException("Invalid resource");
});

admissionTestSupport.assertThatThrownBy(admissionController::handle)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid resource");
}

@Test
void mutatesResource() {
AdmissionController<HasMetadata> admissionController =
new AdmissionController<>((resource, operation) -> {
var admissionController =
new AdmissionController<HasMetadata>((resource, operation) -> {
resource.getMetadata().getLabels().putIfAbsent(AdmissionTestSupport.LABEL_KEY,
LABEL_TEST_VALUE);
return resource;
});
admissionTestSupport.mutatesResource(admissionController::handle);
}

@Test
void mutatesResource_whenNotAllowedException() {
var admissionController =
new AdmissionController<>((Mutator<HasMetadata>) (resource, operation) -> {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
});

admissionTestSupport.notAllowedException(admissionController::handle);
}

@Test
void mutatesResource_whenOtherException() {
var admissionController =
new AdmissionController<>((Mutator<HasMetadata>) (resource, operation) -> {
throw new IllegalArgumentException("Invalid resource");
});

admissionTestSupport.assertThatThrownBy(admissionController::handle)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Invalid resource");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import java.util.UUID;
import java.util.function.Function;

import org.assertj.core.api.AbstractThrowableAssert;
import org.assertj.core.api.Assertions;

import io.fabric8.kubernetes.api.model.admission.v1.AdmissionRequest;
import io.fabric8.kubernetes.api.model.admission.v1.AdmissionReview;
import io.fabric8.kubernetes.api.model.apps.Deployment;
Expand Down Expand Up @@ -39,20 +42,37 @@ void validatesResource(Function<AdmissionReview, AdmissionReview> function) {

assertThat(admissionReview.getResponse().getUid())
.isEqualTo(inputAdmissionReview.getRequest().getUid());
assertThat(admissionReview.getResponse().getStatus().getCode()).isEqualTo(403);
assertThat(admissionReview.getResponse().getStatus().getMessage())
.isEqualTo(MISSING_REQUIRED_LABEL);
assertThat(admissionReview.getResponse().getAllowed()).isFalse();
assertThat(admissionReview.getResponse().getAllowed()).isTrue();
assertThat(admissionReview.getResponse().getStatus()).isNull();
}

void mutatesResource(Function<AdmissionReview, AdmissionReview> function) {
var inputAdmissionReview = createTestAdmissionReview();
var admissionReview = function.apply(inputAdmissionReview);

assertThat(admissionReview.getResponse().getUid())
.isEqualTo(inputAdmissionReview.getRequest().getUid());
assertThat(admissionReview.getResponse().getAllowed()).isTrue();
var patch = new String(Base64.getDecoder().decode(admissionReview.getResponse().getPatch()));
assertThat(patch).isEqualTo(
"[{\"op\":\"add\",\"path\":\"/metadata/labels/app.kubernetes.io~1name\",\"value\":\"mutation-test\"}]");
}

void notAllowedException(Function<AdmissionReview, AdmissionReview> function) {
var inputAdmissionReview = createTestAdmissionReview();
var admissionReview = function.apply(inputAdmissionReview);

assertThat(admissionReview.getResponse().getUid())
.isEqualTo(inputAdmissionReview.getRequest().getUid());
assertThat(admissionReview.getResponse().getAllowed()).isFalse();
assertThat(admissionReview.getResponse().getStatus().getCode()).isEqualTo(403);
assertThat(admissionReview.getResponse().getStatus().getMessage()).isEqualTo(
MISSING_REQUIRED_LABEL);
}

AbstractThrowableAssert<?, ? extends Throwable> assertThatThrownBy(
Function<AdmissionReview, AdmissionReview> function) {
var inputAdmissionReview = createTestAdmissionReview();
return Assertions.assertThatThrownBy(() -> function.apply(inputAdmissionReview));
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,75 @@
package io.javaoperatorsdk.webhook.admission;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.CompletionException;

import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.webhook.admission.mutation.AsyncMutator;
import io.javaoperatorsdk.webhook.admission.mutation.Mutator;
import io.javaoperatorsdk.webhook.admission.validation.Validator;

import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.*;
import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.LABEL_KEY;
import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.LABEL_TEST_VALUE;
import static io.javaoperatorsdk.webhook.admission.AdmissionTestSupport.MISSING_REQUIRED_LABEL;

class AsyncAdmissionControllerTest {

AdmissionTestSupport admissionTestSupport = new AdmissionTestSupport();

@Test
void validatesResource() throws ExecutionException, InterruptedException {
AsyncAdmissionController<HasMetadata> admissionController =
new AsyncAdmissionController<>((resource, operation) -> {
if (resource.getMetadata().getLabels().get(LABEL_KEY) == null) {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
}
});

void validatesResource() {
var admissionController = new AsyncAdmissionController<HasMetadata>((resource, operation) -> {
});

admissionTestSupport
.validatesResource(res -> admissionController.handle(res).toCompletableFuture().join());
}

@Test
void validatesResource_whenNotAllowedException() {
var admissionController =
new AsyncAdmissionController<>((Validator<HasMetadata>) (resource, operation) -> {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
});

admissionTestSupport
.notAllowedException(res -> admissionController.handle(res).toCompletableFuture().join());
}

@Test
void validatesResource_whenOtherException() {
var admissionController =
new AsyncAdmissionController<>((Validator<HasMetadata>) (resource, operation) -> {
throw new IllegalArgumentException("Invalid resource");
});

admissionTestSupport.assertThatThrownBy(
res -> admissionController.handle(res).toCompletableFuture()
.join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(IllegalStateException.class)
.hasRootCauseInstanceOf(IllegalArgumentException.class)
.hasRootCauseMessage("Invalid resource");
}

@Test
void mutatesResource() throws ExecutionException, InterruptedException {
AsyncAdmissionController<HasMetadata> admissionController =
void mutatesResource_withMutator() {
var admissionController =
new AsyncAdmissionController<>((Mutator<HasMetadata>) (resource,
operation) -> {
resource.getMetadata().getLabels().putIfAbsent(LABEL_KEY, LABEL_TEST_VALUE);
return resource;
});

admissionTestSupport
.mutatesResource(res -> admissionController.handle(res).toCompletableFuture().join());
}

@Test
void mutatesResource_withAsyncMutator() {
var admissionController =
new AsyncAdmissionController<>((AsyncMutator<HasMetadata>) (resource,
operation) -> CompletableFuture.supplyAsync(() -> {
resource.getMetadata().getLabels().putIfAbsent(LABEL_KEY, LABEL_TEST_VALUE);
Expand All @@ -43,4 +80,61 @@ void mutatesResource() throws ExecutionException, InterruptedException {
.mutatesResource(res -> admissionController.handle(res).toCompletableFuture().join());
}

@Test
void mutatesResource_withMutator_whenNotAllowedException() {
var admissionController =
new AsyncAdmissionController<>((Mutator<HasMetadata>) (resource,
operation) -> {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
});

admissionTestSupport.notAllowedException(
res -> admissionController.handle(res).toCompletableFuture().join());
}

@Test
void mutatesResource_withAsyncMutator_whenNotAllowedException() {
var admissionController =
new AsyncAdmissionController<>((AsyncMutator<HasMetadata>) (resource,
operation) -> CompletableFuture.supplyAsync(() -> {
throw new NotAllowedException(MISSING_REQUIRED_LABEL);
}));

admissionTestSupport.notAllowedException(
res -> admissionController.handle(res).toCompletableFuture().join());
}

@Test
void mutatesResource_withMutator_whenOtherException() {
var admissionController =
new AsyncAdmissionController<>((Mutator<HasMetadata>) (resource,
operation) -> {
throw new IllegalArgumentException("Invalid resource");
});

admissionTestSupport.assertThatThrownBy(
res -> admissionController.handle(res).toCompletableFuture()
.join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(IllegalStateException.class)
.hasRootCauseInstanceOf(IllegalArgumentException.class)
.hasRootCauseMessage("Invalid resource");
}

@Test
void mutatesResource_withAsyncMutator_whenOtherException() {
var admissionController =
new AsyncAdmissionController<>((AsyncMutator<HasMetadata>) (resource,
operation) -> CompletableFuture.supplyAsync(() -> {
throw new IllegalArgumentException("Invalid resource");
}));

admissionTestSupport.assertThatThrownBy(
res -> admissionController.handle(res).toCompletableFuture()
.join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(IllegalStateException.class)
.hasRootCauseInstanceOf(IllegalArgumentException.class)
.hasRootCauseMessage("Invalid resource");
}
}
Loading

0 comments on commit d530946

Please sign in to comment.