Skip to content

Commit

Permalink
Make PropagationPolicy configurable when deletingExisting is configured
Browse files Browse the repository at this point in the history
await for deletion if the just-deleted resource still exists when trying to create
  • Loading branch information
bbeaudreault committed Dec 17, 2020
1 parent cbc5d52 commit 173ab63
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Fix #2656: Binding operations can be instantiated

#### Improvements
* Fix: Allow specifying PropagationPolicy when using deleteExisting
* Fix: CustomResourceDefinitionContext.fromCrd support for v1 CustomResourceDefinition
* Fix #2642: Update kubernetes-examples to use apps/v1 Deployment rather than extensions/v1beta1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
/**
* Created by iocanel on 9/15/16.
*/
public interface CascadingDeletable extends Deletable, Cascading<Deletable> {
public interface CascadingDeletable<T> extends Deletable, Cascading<Deletable>, Recreateable<Applicable<T>> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@
public interface ListVisitFromServerGetDeleteRecreateWaitApplicable<T> extends Visitable<ListVisitFromServerGetDeleteRecreateWaitApplicable<T>>,
FromServerGettable<List<T>>,
RecreateApplicable<List<T>, T>,
CascadingDeletable,
CascadingDeletable<List<T>>,
Waitable<List<T>, T>,
GracePeriodConfigurable<CascadingDeletable>,
PropagationPolicyConfigurable<CascadingDeletable> {
GracePeriodConfigurable<CascadingDeletable<List<T>>>,
PropagationPolicyConfigurable<CascadingDeletable<List<T>>> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@

public interface VisitFromServerGetWatchDeleteRecreateWaitApplicable<T> extends Visitable<VisitFromServerGetWatchDeleteRecreateWaitApplicable<T>>,
FromServerGettable<T>, RecreateApplicable<T, T>,
CascadingDeletable,
CascadingDeletable<T>,
Watchable<Watcher<T>>,
Waitable<T, T>,
GracePeriodConfigurable<CascadingDeletable>,
PropagationPolicyConfigurable<CascadingDeletable> {
GracePeriodConfigurable<CascadingDeletable<T>>,
PropagationPolicyConfigurable<CascadingDeletable<T>> {
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,12 @@ public VisitFromServerGetWatchDeleteRecreateWaitApplicable<HasMetadata> accept(V
}

@Override
public CascadingDeletable withGracePeriod(long gracePeriodSeconds) {
public CascadingDeletable<HasMetadata> withGracePeriod(long gracePeriodSeconds) {
return new NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl(client, config, fallbackNamespace, explicitNamespace, fromServer, true, visitors, item, gracePeriodSeconds, propagationPolicy, cascading, watchRetryInitialBackoffMillis, watchRetryBackoffMultiplier);
}

@Override
public CascadingDeletable withPropagationPolicy(DeletionPropagation propagationPolicy) {
public CascadingDeletable<HasMetadata> withPropagationPolicy(DeletionPropagation propagationPolicy) {
return new NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl(client, config, fallbackNamespace, explicitNamespace, fromServer, true, visitors, item, gracePeriodSeconds, propagationPolicy, cascading, watchRetryInitialBackoffMillis, watchRetryBackoffMultiplier);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,12 +382,12 @@ public ListVisitFromServerGetDeleteRecreateWaitApplicable<HasMetadata> accept(Vi
return new NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl(client, config, fallbackNamespace, explicitNamespace, fromServer, true, newVisitors, item, null, null, gracePeriodSeconds, propagationPolicy, cascading, watchRetryInitialBackoffMillis, watchRetryBackoffMultiplier);
}

@Override public CascadingDeletable withGracePeriod(long gracePeriodSeconds)
@Override public CascadingDeletable<List<HasMetadata>> withGracePeriod(long gracePeriodSeconds)
{
return new NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl(client, config, fallbackNamespace, explicitNamespace, fromServer, true, visitors, item, null, null, gracePeriodSeconds, propagationPolicy, cascading, watchRetryInitialBackoffMillis, watchRetryBackoffMultiplier);
}

@Override public CascadingDeletable withPropagationPolicy(DeletionPropagation propagationPolicy)
@Override public CascadingDeletable<List<HasMetadata>> withPropagationPolicy(DeletionPropagation propagationPolicy)
{
return new NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl(client, config, fallbackNamespace, explicitNamespace, fromServer, true, visitors, item, null, null, gracePeriodSeconds, propagationPolicy, cascading, watchRetryInitialBackoffMillis, watchRetryBackoffMultiplier);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,25 @@
import io.fabric8.kubernetes.client.ResourceHandler;
import okhttp3.OkHttpClient;

import java.net.HttpURLConnection;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DeleteAndCreateHelper<T extends HasMetadata> {
private static final Logger LOG = LoggerFactory.getLogger(DeleteAndCreateHelper.class);

private final UnaryOperator<T> createTask;
private final Function<T, Boolean> awaitDeleteTask;
private final Function<T, Boolean> deleteTask;

public DeleteAndCreateHelper(UnaryOperator<T> createTask, Function<T, Boolean> deleteTask) {
public DeleteAndCreateHelper(UnaryOperator<T> createTask, Function<T, Boolean> deleteTask, Function<T, Boolean> awaitDeleteTask) {
this.createTask = createTask;
this.awaitDeleteTask = awaitDeleteTask;
this.deleteTask = deleteTask;
}

Expand All @@ -40,15 +50,40 @@ public T deleteAndCreate(T item) {
if (Boolean.FALSE.equals(deleted)) {
throw new KubernetesClientException("Failed to delete existing item:" + item.getMetadata().getName());
}
return createTask.apply(item);
try {
return createTask.apply(item);
} catch (KubernetesClientException e) {
// depending on the grace period, the object might not actually be deleted by the time we try to create
// if that's the case, give it some time.
if (e.getCode() == HttpURLConnection.HTTP_CONFLICT) {
if (Boolean.FALSE.equals(awaitDeleteTask.apply(item))) {
throw new KubernetesClientException("Timed out waiting for item to be deleted before recreating: " + item.getMetadata().getName(), e);
}
return createTask.apply(item);
}
throw e;
}
}

public static HasMetadata deleteAndCreateItem(OkHttpClient client, Config config, HasMetadata meta, ResourceHandler<HasMetadata, HasMetadataVisitiableBuilder> h, String namespaceToUse, DeletionPropagation propagationPolicy) {
DeleteAndCreateHelper<HasMetadata> deleteAndCreateHelper = new DeleteAndCreateHelper<>(
m -> h.create(client, config, namespaceToUse, m),
m -> h.delete(client, config, namespaceToUse, propagationPolicy, m)
m -> h.delete(client, config, namespaceToUse, propagationPolicy, m),
waitUntilDeletedOrInterrupted(client, config, h, namespaceToUse)
);

return deleteAndCreateHelper.deleteAndCreate(meta);
}

private static <T extends HasMetadata> Function<T, Boolean> waitUntilDeletedOrInterrupted(OkHttpClient client, Config config, ResourceHandler<HasMetadata, HasMetadataVisitiableBuilder> h, String namespaceToUse) {
return m -> {
try {
return h.waitUntilCondition(client, config, namespaceToUse, m, Objects::isNull, 30 , TimeUnit.SECONDS) == null;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.warn("interrupted waiting for item to be deleted, assuming not deleted");
return false;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ void testDeleteAndCreate() {
.thenReturn(getPod());
DeleteAndCreateHelper<Pod> podDeleteAndCreateHelper = new DeleteAndCreateHelper<>(
createPodTask,
deletePodTask,
p -> true
);

Expand All @@ -55,7 +56,7 @@ void testDeleteAndCreate() {

// Then
assertNotNull(podCreated);
assertTrue(deletePodTask.apply(podCreated));
assertTrue(wasPodDeleted.get());
}

@Test
Expand All @@ -67,14 +68,72 @@ void testDeleteAndCreateWhenDeletionFailed() {
HttpURLConnection.HTTP_CONFLICT, new StatusBuilder().withCode(HttpURLConnection.HTTP_CONFLICT).build()));
DeleteAndCreateHelper<Pod> podDeleteAndCreateHelper = new DeleteAndCreateHelper<>(
createPodTask,
p -> false
p -> false,
p -> {
throw new RuntimeException("should not be called because deletion failed");
}
);

// When
Pod podToDeleteAndCreate = getPod();
assertThrows(KubernetesClientException.class,() -> podDeleteAndCreateHelper.deleteAndCreate(podToDeleteAndCreate));
}

@Test
void testDeleteAndCreateWhenDeletionSucceedsButNotFinishedInTime() {
// Given
UnaryOperator<Pod> createPodTask = Mockito.mock(UnaryOperator.class, Mockito.RETURNS_DEEP_STUBS);
when(createPodTask.apply(any())).thenThrow(new KubernetesClientException("The POST operation could not be completed at " +
"this time, please try again",
HttpURLConnection.HTTP_CONFLICT, new StatusBuilder().withCode(HttpURLConnection.HTTP_CONFLICT).build()));
DeleteAndCreateHelper<Pod> podDeleteAndCreateHelper = new DeleteAndCreateHelper<>(
createPodTask,
p -> true, // deletion succeeds
p -> false // but doesn't finish in time
);

// When
Pod podToDeleteAndCreate = getPod();
assertThrows(KubernetesClientException.class,() -> podDeleteAndCreateHelper.deleteAndCreate(podToDeleteAndCreate));
}

@Test
void testDeleteAndCreateAfterWaitingForItemToBeDeleted() {
// Given
AtomicBoolean wasPodDeleted = new AtomicBoolean(false);
Function<Pod, Boolean> deletePodTask = p -> {
wasPodDeleted.set(true);
return true;
};

AtomicBoolean awaitedDeletion = new AtomicBoolean(false);
Function<Pod, Boolean> awaitDeletionTask = p -> {
awaitedDeletion.set(true);
return true;
};

UnaryOperator<Pod> createPodTask = Mockito.mock(UnaryOperator.class, Mockito.RETURNS_DEEP_STUBS);
when(createPodTask.apply(any()))
.thenThrow(new KubernetesClientException("The POST operation could not be completed at " +
"this time, please try again",
HttpURLConnection.HTTP_CONFLICT, new StatusBuilder().withCode(HttpURLConnection.HTTP_CONFLICT).build()))
.thenReturn(getPod());

DeleteAndCreateHelper<Pod> podDeleteAndCreateHelper = new DeleteAndCreateHelper<>(
createPodTask,
deletePodTask,
awaitDeletionTask
);

// When
Pod podCreated = podDeleteAndCreateHelper.deleteAndCreate(getPod());

// Then
assertNotNull(podCreated);
assertTrue(wasPodDeleted.get());
assertTrue(awaitedDeletion.get());
}

private Pod getPod() {
return new PodBuilder()
.withNewMetadata().withName("p1").endMetadata()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.fabric8.kubernetes.api.model.ServiceBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.ReplicaSetList;
import io.fabric8.kubernetes.client.KubernetesClient;
import org.arquillian.cube.kubernetes.api.Session;
import org.arquillian.cube.kubernetes.impl.requirement.RequiresKubernetes;
Expand All @@ -49,6 +50,7 @@
import static junit.framework.TestCase.assertNotNull;
import static org.awaitility.Awaitility.await;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

@RunWith(ArquillianConditionalRunner.class)
Expand Down Expand Up @@ -159,6 +161,65 @@ public void delete() {
assertTrue(client.resource(pod1).inNamespace(session.getNamespace()).delete());
}

@Test
public void testDeleteExistingWithOrphanDeletion() {
// Create Deployment
client.resource(deployment).inNamespace(session.getNamespace()).createOrReplace();
await().atMost(30, TimeUnit.SECONDS).until(resourceIsReady(deployment));

// get creationTimestamp of underlying replicaset. we expect this NOT to match later, meaning the orphan WAS deleted.
ReplicaSetList replicaSetList = client.apps().replicaSets().inNamespace(session.getNamespace()).withLabel("run", "deploy1").list();
assertEquals(1, replicaSetList.getItems().size());
String replicaSetCreationTimestamp = replicaSetList.getItems().get(0).getMetadata().getCreationTimestamp();

// Recreate deployment with deleteExisting
client.resource(deployment).inNamespace(session.getNamespace()).withPropagationPolicy(DeletionPropagation.FOREGROUND).deletingExisting().createOrReplace();
await().atMost(30, TimeUnit.SECONDS).until(resourceIsReady(deployment));

// check that creationTimestamp DOES NOT MATCH original, meaning the orphan WAS deleted
replicaSetList = client.apps().replicaSets().inNamespace(session.getNamespace()).withLabel("run", "deploy1").list();
assertEquals(1, replicaSetList.getItems().size());
assertNotEquals(replicaSetCreationTimestamp, replicaSetList.getItems().get(0).getMetadata().getCreationTimestamp());

// cleanup
assertEquals(true, client.resource(deployment).inNamespace(session.getNamespace()).delete());
// Check whether child resources are also deleted
await().atMost(30, TimeUnit.SECONDS)
.until(() -> client.apps().replicaSets().inNamespace(session.getNamespace()).withLabel("run", "deploy1").list().getItems().size() == 0);
}

@Test
public void testDeleteExistingWithoutOrphanDeletion() {
// Create Deployment
client.resource(deployment).inNamespace(session.getNamespace()).createOrReplace();
await().atMost(30, TimeUnit.SECONDS).until(resourceIsReady(deployment));

// get creationTimestamp of underlying replicaset. we expect this to match later, meaning the orphan was not deleted.
ReplicaSetList replicaSetList = client.apps().replicaSets().inNamespace(session.getNamespace()).withLabel("run", "deploy1").list();
assertEquals(1, replicaSetList.getItems().size());
String replicaSetCreationTimestamp = replicaSetList.getItems().get(0).getMetadata().getCreationTimestamp();

// Recreate deployment with deleteExisting
client.resource(deployment)
.inNamespace(session.getNamespace())
.withPropagationPolicy(DeletionPropagation.ORPHAN)
.deletingExisting()
.createOrReplace();

await().atMost(30, TimeUnit.SECONDS).until(resourceIsReady(deployment));

// check that creationTimestamp matches original, meaning the orphan was not deleted
replicaSetList = client.apps().replicaSets().inNamespace(session.getNamespace()).withLabel("run", "deploy1").list();
assertEquals(1, replicaSetList.getItems().size());
assertEquals(replicaSetCreationTimestamp, replicaSetList.getItems().get(0).getMetadata().getCreationTimestamp());

// cleanup
assertEquals(true, client.resource(deployment).inNamespace(session.getNamespace()).delete());
// Check whether child resources are also deleted
await().atMost(30, TimeUnit.SECONDS)
.until(() -> client.apps().replicaSets().inNamespace(session.getNamespace()).withLabel("run", "deploy1").list().getItems().size() == 0);
}

@Test
public void testDeletionWithOrphanDeletion() {
// Create Deployment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package io.fabric8.kubernetes.client.mock;

import io.fabric8.kubernetes.api.model.DeleteOptions;
import io.fabric8.kubernetes.api.model.DeletionPropagation;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
Expand All @@ -33,6 +35,7 @@
import io.fabric8.kubernetes.client.dsl.PodResource;
import io.fabric8.kubernetes.client.internal.readiness.Readiness;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.fabric8.kubernetes.client.utils.Serialization;
import okhttp3.mockwebserver.RecordedRequest;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.Assert;
Expand Down Expand Up @@ -118,6 +121,30 @@ void testCreateOrReplaceWithDeleteExisting() throws Exception {
assertEquals("POST", request.getMethod());
}

@Test
void itPassesPropagationPolicyWithDeleteExisting() throws InterruptedException {
Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build();

server.expect().delete().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(HttpURLConnection.HTTP_OK, pod1).once();
server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(HttpURLConnection.HTTP_CREATED, pod1).once();

KubernetesClient client = server.getClient();
HasMetadata response = client.resource(pod1).inNamespace("ns1").withPropagationPolicy(DeletionPropagation.FOREGROUND).deletingExisting().createOrReplace();
assertEquals(pod1, response);

assertEquals(2, server.getMockServer().getRequestCount());

RecordedRequest deleteRequest = server.getMockServer().takeRequest();
assertEquals("/api/v1/namespaces/ns1/pods/pod1", deleteRequest.getPath());
assertEquals("DELETE", deleteRequest.getMethod());
DeleteOptions deleteOptions = Serialization.unmarshal(deleteRequest.getBody().readUtf8(), DeleteOptions.class);
assertEquals("Foreground", deleteOptions.getPropagationPolicy());

RecordedRequest postRequest = server.getLastRequest();
assertEquals("/api/v1/namespaces/ns1/pods", postRequest.getPath());
assertEquals("POST", postRequest.getMethod());
}

@Test
void testCreateOrReplaceWithDeleteExistingWithCreateFailed() {
// Given
Expand Down

0 comments on commit 173ab63

Please sign in to comment.