From b6d9ba7f805981d63446a7f8a056f0dd5bc6637c Mon Sep 17 00:00:00 2001 From: Florian Wiesner Date: Fri, 18 Sep 2020 12:52:16 +0200 Subject: [PATCH 1/2] Workaround for #2292 --- .../client/dsl/base/BaseOperation.java | 40 ++++++++++++++----- .../kubernetes/client/utils/Utils.java | 1 + 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java index 6b1b4eff704..935dd89c3e5 100755 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java @@ -71,6 +71,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -405,19 +406,36 @@ public final T createOrReplace(T... items) { return withName(itemToCreateOrReplace.getMetadata().getName()).createOrReplace(itemToCreateOrReplace); } - try { - // Create - KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, null); - return create(itemToCreateOrReplace); - } catch (KubernetesClientException exception) { - if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { - throw exception; + final CompletableFuture future = new CompletableFuture<>(); + while (!future.isDone()) { + try { + // Create + KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, null); + future.complete(create(itemToCreateOrReplace)); + } catch (KubernetesClientException exception) { + final T itemFromServer; + if (exception.getCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) { + itemFromServer = fromServer().get(); + if (itemFromServer == null) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + continue; + } + } else if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { + throw exception; + } else { + itemFromServer = fromServer().get(); + } + + // Conflict; Do Replace + KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, KubernetesResourceUtil.getResourceVersion(itemFromServer)); + future.complete(replace(itemToCreateOrReplace)); } - // Conflict; Do Replace - final T itemFromServer = fromServer().get(); - KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, KubernetesResourceUtil.getResourceVersion(itemFromServer)); - return replace(itemToCreateOrReplace); } + return future.join(); } @Override diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java index e1deab12fe1..99a90c2315e 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java @@ -358,6 +358,7 @@ public static String getPluralFromKind(String kind) { break; case "NetworkPolicy": case "PodSecurityPolicy": + case "ServiceEntry": // an Istio resource. Needed as getPluralFromKind is currently not configurable #2489 // Delete last character pluralBuffer.deleteCharAt(pluralBuffer.length() - 1); pluralBuffer.append("ies"); From 2aaa79bc719491dd0efbe761f98bef15f1e86ede Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Tue, 20 Oct 2020 15:30:46 +0530 Subject: [PATCH 2/2] Changes in BaseOperation to retry createOrReplace on server failure + Moved createOrReplace logic to CreateOrReplaceHelper class + Added tests --- .../client/dsl/base/BaseOperation.java | 46 ++---- ...WatchDeleteRecreateWaitApplicableImpl.java | 31 +--- ...hDeleteRecreateWaitApplicableListImpl.java | 39 ++--- .../client/utils/CreateOrReplaceHelper.java | 98 ++++++++++++ .../client/utils/DeleteAndCreateHelper.java | 54 +++++++ .../kubernetes/client/utils/Utils.java | 1 + .../client/dsl/base/BaseOperationTest.java | 15 +- .../utils/CreateOrReplaceHelperTest.java | 140 ++++++++++++++++++ .../utils/DeleteAndCreateHelperTest.java | 83 +++++++++++ .../fabric8/kubernetes/CreateOrReplaceIT.java | 125 ++++++++++++++-- .../createorreplace-it-testlist-v1.yml | 38 +++++ .../createorreplace-it-testlist-v2.yml | 39 +++++ .../client/mock/ResourceListTest.java | 4 +- .../kubernetes/client/mock/ResourceTest.java | 3 +- 14 files changed, 608 insertions(+), 108 deletions(-) create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelper.java create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelper.java create mode 100644 kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelperTest.java create mode 100644 kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelperTest.java create mode 100644 kubernetes-itests/src/test/resources/createorreplace-it-testlist-v1.yml create mode 100644 kubernetes-itests/src/test/resources/createorreplace-it-testlist-v2.yml diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java index 935dd89c3e5..d4886225c39 100755 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/BaseOperation.java @@ -16,7 +16,7 @@ package io.fabric8.kubernetes.client.dsl.base; import io.fabric8.kubernetes.api.model.ObjectReference; -import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.fabric8.kubernetes.client.utils.CreateOrReplaceHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +71,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -405,37 +404,22 @@ public final T createOrReplace(T... items) { return withName(itemToCreateOrReplace.getMetadata().getName()).createOrReplace(itemToCreateOrReplace); } - - final CompletableFuture future = new CompletableFuture<>(); - while (!future.isDone()) { - try { - // Create - KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, null); - future.complete(create(itemToCreateOrReplace)); - } catch (KubernetesClientException exception) { - final T itemFromServer; - if (exception.getCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) { - itemFromServer = fromServer().get(); - if (itemFromServer == null) { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - continue; - } - } else if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { - throw exception; - } else { - itemFromServer = fromServer().get(); + T finalItemToCreateOrReplace = itemToCreateOrReplace; + CreateOrReplaceHelper createOrReplaceHelper = new CreateOrReplaceHelper<>( + this::create, + this::replace, + m -> { + try { + return waitUntilCondition(Objects::nonNull, 1, TimeUnit.SECONDS); + } catch (InterruptedException interruptedException) { + interruptedException.printStackTrace(); } + return null; + }, + m -> fromServer().get() + ); - // Conflict; Do Replace - KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, KubernetesResourceUtil.getResourceVersion(itemFromServer)); - future.complete(replace(itemToCreateOrReplace)); - } - } - return future.join(); + return createOrReplaceHelper.createOrReplace(finalItemToCreateOrReplace); } @Override diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl.java index 2012bfef257..5b76cfc0e9a 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl.java @@ -17,10 +17,8 @@ import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ListOptions; -import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Utils; -import java.net.HttpURLConnection; import java.util.function.Predicate; import java.io.ByteArrayInputStream; @@ -57,6 +55,9 @@ import io.fabric8.kubernetes.client.internal.readiness.Readiness; import okhttp3.OkHttpClient; +import static io.fabric8.kubernetes.client.utils.CreateOrReplaceHelper.createOrReplaceItem; +import static io.fabric8.kubernetes.client.utils.DeleteAndCreateHelper.deleteAndCreateItem; + public class NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableImpl extends OperationSupport implements NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicable, Waitable, @@ -135,29 +136,10 @@ public HasMetadata createOrReplace() { HasMetadata meta = acceptVisitors(asHasMetadata(item), visitors); ResourceHandler h = handlerOf(meta); String namespaceToUse = meta.getMetadata().getNamespace(); - - String resourceVersion = KubernetesResourceUtil.getResourceVersion(meta); - try { - // Create - KubernetesResourceUtil.setResourceVersion(meta, null); - return h.create(client, config, namespaceToUse, meta); - } catch (KubernetesClientException exception) { - if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { - throw exception; - } - - // Conflict; check deleteExisting flag otherwise replace - if (Boolean.TRUE.equals(deletingExisting)) { - Boolean deleted = h.delete(client, config, namespaceToUse, propagationPolicy, meta); - if (Boolean.FALSE.equals(deleted)) { - throw new KubernetesClientException("Failed to delete existing item:" + meta); - } - return h.create(client, config, namespaceToUse, meta); - } else { - KubernetesResourceUtil.setResourceVersion(meta, resourceVersion); - return h.replace(client, config, namespaceToUse, meta); - } + if (Boolean.TRUE.equals(deletingExisting)) { + return deleteAndCreateItem(client, config, meta, h, namespaceToUse, propagationPolicy); } + return createOrReplaceItem(client, config, meta, h, namespaceToUse); } @Override @@ -327,5 +309,4 @@ private static ResourceHandler handlerOf(T item) { throw new IllegalArgumentException("Could not find a registered handler for item: [" + item + "]."); } } - } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java index 95f707fb12a..b8a4fb1c8cc 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java @@ -34,7 +34,6 @@ import io.fabric8.kubernetes.client.dsl.base.OperationSupport; import io.fabric8.kubernetes.client.handlers.KubernetesListHandler; import io.fabric8.kubernetes.client.internal.readiness.Readiness; -import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Utils; @@ -45,7 +44,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.net.HttpURLConnection; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; @@ -62,6 +60,9 @@ import java.util.concurrent.TimeoutException; import java.util.function.Predicate; +import static io.fabric8.kubernetes.client.utils.CreateOrReplaceHelper.createOrReplaceItem; +import static io.fabric8.kubernetes.client.utils.DeleteAndCreateHelper.deleteAndCreateItem; + public class NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl extends OperationSupport implements ParameterNamespaceListVisitFromServerGetDeleteRecreateWaitApplicable, Waitable, HasMetadata>, Readiable { @@ -285,29 +286,11 @@ public List createOrReplace() { List result = new ArrayList<>(); for (HasMetadata meta : acceptVisitors(asHasMetadata(item, true), visitors)) { ResourceHandler h = handlerOf(meta); - String namespaceToUse = meta.getMetadata().getNamespace(); + String namespaceToUse = meta.getMetadata().getNamespace(); - String resourceVersion = KubernetesResourceUtil.getResourceVersion(meta); - try { - // Create - KubernetesResourceUtil.setResourceVersion(meta, null); - result.add(h.create(client, config, namespaceToUse, meta)); - } catch (KubernetesClientException exception) { - if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { - throw exception; - } - - // Conflict; check deleteExisting flag otherwise replace - if (Boolean.TRUE.equals(deletingExisting)) { - Boolean deleted = h.delete(client, config, namespaceToUse, propagationPolicy, meta); - if (Boolean.FALSE.equals(deleted)) { - throw new KubernetesClientException("Failed to delete existing item:" + meta); - } - result.add(h.create(client, config, namespaceToUse, meta)); - } else { - KubernetesResourceUtil.setResourceVersion(meta, resourceVersion); - result.add(h.replace(client, config, namespaceToUse, meta)); - } + HasMetadata createdItem = createOrReplaceOrDeleteExisting(meta, h, namespaceToUse); + if (createdItem != null) { + result.add(createdItem); } } return result; @@ -455,4 +438,12 @@ private static ResourceHandler handlerOf(T item) { throw new IllegalArgumentException("Could not find a registered handler for item: [" + item + "]."); } } + + private HasMetadata createOrReplaceOrDeleteExisting(HasMetadata meta, ResourceHandler h, String namespaceToUse) { + if (Boolean.TRUE.equals(deletingExisting)) { + return deleteAndCreateItem(client, config, meta, h, namespaceToUse, propagationPolicy); + } + return createOrReplaceItem(client, config, meta, h, namespaceToUse); + } + } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelper.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelper.java new file mode 100644 index 00000000000..cd8e46a2392 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelper.java @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.utils; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.HasMetadataVisitiableBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.ResourceHandler; +import okhttp3.OkHttpClient; + +import java.net.HttpURLConnection; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.UnaryOperator; + +public class CreateOrReplaceHelper { + public static final int CREATE_OR_REPLACE_RETRIES = 3; + private final UnaryOperator createTask; + private final UnaryOperator replaceTask; + private final UnaryOperator waitTask; + private final UnaryOperator reloadTask; + + public CreateOrReplaceHelper(UnaryOperator createTask, UnaryOperator replaceTask, UnaryOperator waitTask, UnaryOperator reloadTask) { + this.createTask = createTask; + this.replaceTask = replaceTask; + this.waitTask = waitTask; + this.reloadTask = reloadTask; + } + + public T createOrReplace(T item) { + String resourceVersion = KubernetesResourceUtil.getResourceVersion(item); + final CompletableFuture future = new CompletableFuture<>(); + int nTries = 0; + while (!future.isDone() && nTries < CREATE_OR_REPLACE_RETRIES) { + try { + // Create + KubernetesResourceUtil.setResourceVersion(item, null); + return createTask.apply(item); + } catch (KubernetesClientException exception) { + if (shouldRetry(exception.getCode())) { + T itemFromServer = reloadTask.apply(item); + if (itemFromServer == null) { + waitTask.apply(item); + nTries++; + continue; + } + } else if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { + throw exception; + } + + future.complete(replace(item, resourceVersion)); + } + } + return future.join(); + } + + public static HasMetadata createOrReplaceItem(OkHttpClient client, Config config, HasMetadata meta, ResourceHandler h, String namespaceToUse) { + CreateOrReplaceHelper createOrReplaceHelper = new CreateOrReplaceHelper<>( + m -> h.create(client, config, namespaceToUse, m), + m -> h.replace(client, config, namespaceToUse, m), + m -> { + try { + return h.waitUntilCondition(client, config, namespaceToUse, m, Objects::nonNull, 1, TimeUnit.SECONDS); + } catch (InterruptedException interruptedException) { + interruptedException.printStackTrace(); + } + return null; + }, + m -> h.reload(client, config, namespaceToUse, m) + ); + + return createOrReplaceHelper.createOrReplace(meta); + } + + private T replace(T item, String resourceVersion) { + KubernetesResourceUtil.setResourceVersion(item, resourceVersion); + return replaceTask.apply(item); + } + + private boolean shouldRetry(int responseCode) { + return responseCode > 499; + } +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelper.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelper.java new file mode 100644 index 00000000000..da3502579c5 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelper.java @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.utils; + +import io.fabric8.kubernetes.api.model.DeletionPropagation; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.HasMetadataVisitiableBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.ResourceHandler; +import okhttp3.OkHttpClient; + +import java.util.function.Function; +import java.util.function.UnaryOperator; + +public class DeleteAndCreateHelper { + private final UnaryOperator createTask; + private final Function deleteTask; + + public DeleteAndCreateHelper(UnaryOperator createTask, Function deleteTask) { + this.createTask = createTask; + this.deleteTask = deleteTask; + } + + public T deleteAndCreate(T item) { + Boolean deleted = deleteTask.apply(item); + if (Boolean.FALSE.equals(deleted)) { + throw new KubernetesClientException("Failed to delete existing item:" + item.getMetadata().getName()); + } + return createTask.apply(item); + } + + public static HasMetadata deleteAndCreateItem(OkHttpClient client, Config config, HasMetadata meta, ResourceHandler h, String namespaceToUse, DeletionPropagation propagationPolicy) { + DeleteAndCreateHelper deleteAndCreateHelper = new DeleteAndCreateHelper<>( + m -> h.create(client, config, namespaceToUse, m), + m -> h.delete(client, config, namespaceToUse, propagationPolicy, m) + ); + + return deleteAndCreateHelper.deleteAndCreate(meta); + } +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java index 99a90c2315e..8c7aa2192b9 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Utils.java @@ -472,4 +472,5 @@ public static List getCommandPlatformPrefix() { private static String getOperatingSystemFromSystemProperty() { return System.getProperty(OS_NAME); } + } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/base/BaseOperationTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/base/BaseOperationTest.java index b8d371e822d..b9e2a89fa21 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/base/BaseOperationTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/base/BaseOperationTest.java @@ -38,16 +38,13 @@ import io.fabric8.kubernetes.client.utils.URLUtils; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.client.Watch; -import io.fabric8.kubernetes.client.Watcher; -import io.fabric8.kubernetes.client.dsl.FilterWatchListDeletable; import io.fabric8.kubernetes.client.dsl.internal.PodOperationContext; import io.fabric8.kubernetes.client.dsl.internal.core.v1.PodOperationsImpl; -public class BaseOperationTest { +class BaseOperationTest { @Test - public void testSimpleFieldQueryParamConcatenation() { + void testSimpleFieldQueryParamConcatenation() { Map fieldsMap = new HashMap<>(); fieldsMap.put("yesKey1", "yesValue1"); fieldsMap.put("yesKey2", "yesValue2"); @@ -68,7 +65,7 @@ public void testSimpleFieldQueryParamConcatenation() { } @Test - public void testSkippingFieldNotMatchingNullValues() { + void testSkippingFieldNotMatchingNullValues() { final PodOperationsImpl operation = new PodOperationsImpl(new PodOperationContext()); operation .withField("key1", "value1") @@ -83,14 +80,14 @@ public void testSkippingFieldNotMatchingNullValues() { } @Test - public void testDefaultGracePeriod() { + void testDefaultGracePeriod() { final BaseOperation operation = new BaseOperation(new OperationContext()); assertThat(operation.getGracePeriodSeconds(), is(-1L)); } @Test - public void testChainingGracePeriodAndPropagationPolicy() { + void testChainingGracePeriodAndPropagationPolicy() { final BaseOperation operation = new BaseOperation(new OperationContext()); EditReplacePatchDeletable operationWithPropagationPolicy = operation.withPropagationPolicy(DeletionPropagation.FOREGROUND); assertThat(operationWithPropagationPolicy, is(notNullValue())); @@ -98,7 +95,7 @@ public void testChainingGracePeriodAndPropagationPolicy() { } @Test - public void testListOptions() throws MalformedURLException { + void testListOptions() throws MalformedURLException { // Given URL url = new URL("https://172.17.0.2:8443/api/v1/namespaces/default/pods"); final BaseOperation> operation = new BaseOperation<>(new OperationContext()); diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelperTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelperTest.java new file mode 100644 index 00000000000..627d54a11e1 --- /dev/null +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/CreateOrReplaceHelperTest.java @@ -0,0 +1,140 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.utils; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.StatusBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.net.HttpURLConnection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class CreateOrReplaceHelperTest { + + @Test + void testCreateOrReplaceShouldCreate() { + // Given + AtomicBoolean wasPodCreated = new AtomicBoolean(false); + UnaryOperator createPodTask = p -> { + wasPodCreated.set(true); + return getPod(); + }; + CreateOrReplaceHelper podCreateOrReplaceHelper = new CreateOrReplaceHelper<>( + createPodTask, + p -> getPod(), + p -> getPod(), + p -> getPod() + ); + + // When + Pod podCreated = podCreateOrReplaceHelper.createOrReplace(getPod()); + + // Then + assertNotNull(podCreated); + assertTrue(wasPodCreated.get()); + } + + @Test + void testCreateOrReplaceShouldReplace() { + // Given + AtomicBoolean wasPodReplaced = new AtomicBoolean(false); + UnaryOperator createPodTask = p -> { + throw new KubernetesClientException("Already exist", + HttpURLConnection.HTTP_CONFLICT, new StatusBuilder().withCode(HttpURLConnection.HTTP_CONFLICT).build()); + }; + UnaryOperator replacePodTask = p -> { + wasPodReplaced.set(true); + return getPod(); + }; + CreateOrReplaceHelper podCreateOrReplaceHelper = new CreateOrReplaceHelper<>( + createPodTask, + replacePodTask, + p -> getPod(), + p -> getPod() + ); + + // When + Pod podCreated = podCreateOrReplaceHelper.createOrReplace(getPod()); + + // Then + assertNotNull(podCreated); + assertTrue(wasPodReplaced.get()); + } + + @Test + void testCreateOrReplaceShouldRetryOnInternalServerError() { + // Given + AtomicBoolean waitedForPod = new AtomicBoolean(false); + UnaryOperator createPodTask = Mockito.mock(UnaryOperator.class, Mockito.RETURNS_DEEP_STUBS); + UnaryOperator reloadTask = Mockito.mock(UnaryOperator.class, Mockito.RETURNS_DEEP_STUBS); + when(reloadTask.apply(any())).thenReturn(null); + when(createPodTask.apply(any())).thenThrow(new KubernetesClientException("The POST operation could not be completed at " + + "this time, please try again", + HttpURLConnection.HTTP_INTERNAL_ERROR, new StatusBuilder().withCode(HttpURLConnection.HTTP_INTERNAL_ERROR).build())) + .thenReturn(getPod()); + UnaryOperator waitTask = p -> { + waitedForPod.set(true); + return getPod(); + }; + CreateOrReplaceHelper podCreateOrReplaceHelper = new CreateOrReplaceHelper<>( + createPodTask, + p -> getPod(), + waitTask, + reloadTask + ); + + // When + Pod podCreated = podCreateOrReplaceHelper.createOrReplace(getPod()); + + // Then + assertNotNull(podCreated); + assertTrue(waitedForPod.get()); + } + + @Test + void testCreateOrReplaceThrowExceptionOnErrorCodeLessThan500() { + // Given + UnaryOperator createPodTask = p -> { + throw new KubernetesClientException("The POST operation could not be completed at " + + "this time, please try again", + HttpURLConnection.HTTP_BAD_REQUEST, new StatusBuilder().withCode(HttpURLConnection.HTTP_BAD_REQUEST).build()); + }; + CreateOrReplaceHelper podCreateOrReplaceHelper = new CreateOrReplaceHelper<>(createPodTask, + p -> getPod(), p ->getPod(), p -> getPod()); + Pod podToCreate = getPod(); + + // When + assertThrows(KubernetesClientException.class, () -> podCreateOrReplaceHelper.createOrReplace(podToCreate)); + } + + + private Pod getPod() { + return new PodBuilder() + .withNewMetadata().withName("p1").endMetadata() + .build(); + } +} diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelperTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelperTest.java new file mode 100644 index 00000000000..f0fa06b33a8 --- /dev/null +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/DeleteAndCreateHelperTest.java @@ -0,0 +1,83 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.fabric8.kubernetes.client.utils; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.StatusBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.net.HttpURLConnection; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import static org.junit.Assert.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class DeleteAndCreateHelperTest { + @Test + void testDeleteAndCreate() { + // Given + AtomicBoolean wasPodDeleted = new AtomicBoolean(false); + Function deletePodTask = p -> { + wasPodDeleted.set(true); + return true; + }; + UnaryOperator createPodTask = Mockito.mock(UnaryOperator.class, Mockito.RETURNS_DEEP_STUBS); + when(createPodTask.apply(any())) + .thenReturn(getPod()); + DeleteAndCreateHelper podDeleteAndCreateHelper = new DeleteAndCreateHelper<>( + createPodTask, + p -> true + ); + + // When + Pod podCreated = podDeleteAndCreateHelper.deleteAndCreate(getPod()); + + // Then + assertNotNull(podCreated); + assertTrue(deletePodTask.apply(podCreated)); + } + + @Test + void testDeleteAndCreateWhenDeletionFailed() { + // Given + UnaryOperator 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 podDeleteAndCreateHelper = new DeleteAndCreateHelper<>( + createPodTask, + p -> false + ); + + // When + Pod podToDeleteAndCreate = getPod(); + assertThrows(KubernetesClientException.class,() -> podDeleteAndCreateHelper.deleteAndCreate(podToDeleteAndCreate)); + } + + private Pod getPod() { + return new PodBuilder() + .withNewMetadata().withName("p1").endMetadata() + .build(); + } +} diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/CreateOrReplaceIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/CreateOrReplaceIT.java index 01b086b3bb5..c86926d40ae 100644 --- a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/CreateOrReplaceIT.java +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/CreateOrReplaceIT.java @@ -17,6 +17,7 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.Secret; import io.fabric8.kubernetes.api.model.SecretBuilder; @@ -33,15 +34,22 @@ import org.arquillian.cube.kubernetes.impl.requirement.RequiresKubernetes; import org.arquillian.cube.requirement.ArquillianConditionalRunner; import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.InputStream; +import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Optional; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @RunWith(ArquillianConditionalRunner.class) @RequiresKubernetes @@ -52,6 +60,9 @@ public class CreateOrReplaceIT { @ArquillianResource Session session; + private HasMetadata resource = null; + private List resourceList = null; + @Test public void testCreateOrReplaceConfigMap() { ConfigMap configMap = new ConfigMapBuilder() @@ -76,9 +87,6 @@ public void testCreateOrReplaceConfigMap() { assertNotNull(configMap); assertEquals("2nd", configMap.getData().get("second")); assertEquals("3rd", configMap.getData().get("third")); - - // Cleanup - client.configMaps().inNamespace(session.getNamespace()).withName(configMap.getMetadata().getName()).delete(); } @Test @@ -113,9 +121,6 @@ public void testCreateOrReplaceService() { service = client.services().inNamespace(session.getNamespace()).createOrReplace(service); assertNotNull(service); assertEquals(9090, service.getSpec().getPorts().get(0).getTargetPort().getIntVal().intValue()); - - // Cleanup - client.services().inNamespace(session.getNamespace()).withName(service.getMetadata().getName()).delete(); } @Test @@ -159,9 +164,6 @@ public void testCreateOrReplaceDeployment() { deployment = client.apps().deployments().inNamespace(session.getNamespace()).createOrReplace(deployment); assertNotNull(deployment); assertEquals("busybox:1.32", deployment.getSpec().getTemplate().getSpec().getContainers().get(0).getImage()); - - // Cleanup - client.apps().deployments().inNamespace(session.getNamespace()).withName(deployment.getMetadata().getName()).delete(); } @Test @@ -187,9 +189,6 @@ public void testCreateOrReplaceSecret() { secret = client.secrets().inNamespace(session.getNamespace()).createOrReplace(secret); assertNotNull(secret); assertEquals("c29tZXRva2Vu", secret.getData().get("apitoken")); - - // Cleanup - client.secrets().inNamespace(session.getNamespace()).withName(secret.getMetadata().getName()).delete(); } @Test @@ -227,12 +226,110 @@ public void testCreateOrReplaceIngress() { ingress = client.extensions().ingresses().inNamespace(session.getNamespace()).createOrReplace(ingress); assertNotNull(ingress); assertEquals("/", ingress.getMetadata().getAnnotations().get("nginx.ingress.kubernetes.io/rewrite-target")); + } + + @Test + public void testCreateOrReplaceGenericResource() { + // Given + ConfigMap configMap = new ConfigMapBuilder() + .withNewMetadata().withName("resource-cm-1").endMetadata() + .addToData("a1", "A1") + .addToData("a2", "A2") + .build(); + + // When + ConfigMap createdResource = client.resource(configMap).inNamespace(session.getNamespace()).createOrReplace(); + configMap.setData(Collections.singletonMap("b1", "B1")); + this.resource = client.resource(configMap).inNamespace(session.getNamespace()).createOrReplace(); - // Cleanup - client.network().ingresses().inNamespace(session.getNamespace()).withName(ingress.getMetadata().getName()).delete(); + // Then + assertNotNull(createdResource); + assertEquals(2, createdResource.getData().size()); + assertEquals("A1", createdResource.getData().get("a1")); + assertEquals("A2", createdResource.getData().get("a2")); + ConfigMap replacedResource = (ConfigMap) this.resource; + assertNotNull(replacedResource); + assertEquals(1, replacedResource.getData().size()); + assertEquals("B1", replacedResource.getData().get("b1")); + } + + @Test + public void testCreateOrReplaceGenericResourceList() { + // Given + InputStream resourceListV1 = getClass().getResourceAsStream("/createorreplace-it-testlist-v1.yml"); + InputStream resourceListV2 = getClass().getResourceAsStream("/createorreplace-it-testlist-v2.yml"); + + // When + List listCreated = client.load(resourceListV1).inNamespace(session.getNamespace()).createOrReplace(); + resourceList = client.load(resourceListV2).inNamespace(session.getNamespace()).createOrReplace(); + + // Then + assertNotNull(listCreated); + assertEquals(2, listCreated.size()); + Optional serviceResult = listCreated.stream().filter(p -> p instanceof Service).findFirst(); + assertTrue(serviceResult.isPresent()); + Service service = (Service) serviceResult.get(); + assertEquals(9376, service.getSpec().getPorts().get(0).getTargetPort().getIntVal().intValue()); + + assertNotNull(resourceList); + assertEquals(2, resourceList.size()); + Optional serviceV2Result = resourceList.stream().filter(p -> p instanceof Service).findFirst(); + assertTrue(serviceV2Result.isPresent()); + Service serviceV2 = (Service) serviceV2Result.get(); + assertEquals(9090, serviceV2.getSpec().getPorts().get(0).getTargetPort().getIntVal().intValue()); + } + + @Test + public void testCreateOrReplaceDeletingExisting() { + // Given + List listToCreate = new ArrayList<>(); + listToCreate.add(new ConfigMapBuilder().withNewMetadata().withName("createorreplace-it-delete-existing-configmap").endMetadata() + .addToData("A", "a") + .addToData("B", "b") + .build()); + listToCreate.add(new SecretBuilder().withNewMetadata().withName("createorreplace-it-delete-existing-secret").endMetadata() + .addToData("USERNAME", "YWRtaW4=") + .addToData("PASSWORD", "MWYyZDFlMmU2N2Rm") + .build()); + + // When + List listCreated = client.resourceList(listToCreate).inNamespace(session.getNamespace()).createOrReplace(); + resourceList = client.resourceList(listToCreate) + .inNamespace(session.getNamespace()) + .deletingExisting() + .createOrReplace(); + + // Then + assertNotNull(listCreated); + assertEquals(2, listCreated.size()); + listCreated.sort(Comparator.comparing(HasMetadata::getKind)); + + assertNotNull(resourceList); + assertEquals(2, resourceList.size()); + resourceList.sort(Comparator.comparing(HasMetadata::getKind)); + assertEquals(listCreated.get(0).getMetadata().getName(), resourceList.get(0).getMetadata().getName()); + assertNotEquals(listCreated.get(0).getMetadata().getUid(), resourceList.get(0).getMetadata().getUid()); + assertEquals(listCreated.get(1).getMetadata().getName(), resourceList.get(1).getMetadata().getName()); + assertNotEquals(listCreated.get(1).getMetadata().getUid(), resourceList.get(1).getMetadata().getUid()); } private String getTestResourcePrefix() { return getClass().getSimpleName().toLowerCase(); } + + @After + public void cleanup() { + client.network().ingresses().inNamespace(session.getNamespace()).withName(getTestResourcePrefix() + "-ing").delete(); + client.secrets().inNamespace(session.getNamespace()).withName(getTestResourcePrefix() + "-secret").delete(); + client.apps().deployments().inNamespace(session.getNamespace()).withName(getTestResourcePrefix() + "-deploy").delete(); + client.services().inNamespace(session.getNamespace()).withName(getTestResourcePrefix() + "-svc").delete(); + client.configMaps().inNamespace(session.getNamespace()).withName(getTestResourcePrefix() + "-configmap").delete(); + if (resource != null) { + client.resource(resource).inNamespace(session.getNamespace()).delete(); + } + if (resourceList != null) { + client.resourceList(resourceList).inNamespace(session.getNamespace()).delete(); + } + } + } diff --git a/kubernetes-itests/src/test/resources/createorreplace-it-testlist-v1.yml b/kubernetes-itests/src/test/resources/createorreplace-it-testlist-v1.yml new file mode 100644 index 00000000000..f0f529ac287 --- /dev/null +++ b/kubernetes-itests/src/test/resources/createorreplace-it-testlist-v1.yml @@ -0,0 +1,38 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +--- +apiVersion: v1 +kind: Service +metadata: + name: createorreplace-it-resourcelist-service +spec: + selector: + app: MyApp + ports: + - protocol: TCP + port: 80 + targetPort: 9376 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: createorreplace-it-resourcelist-configmap + namespace: default +data: + allowed: '"true"' + enemies: aliens + lives: "3" diff --git a/kubernetes-itests/src/test/resources/createorreplace-it-testlist-v2.yml b/kubernetes-itests/src/test/resources/createorreplace-it-testlist-v2.yml new file mode 100644 index 00000000000..db9af34051d --- /dev/null +++ b/kubernetes-itests/src/test/resources/createorreplace-it-testlist-v2.yml @@ -0,0 +1,39 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +--- +apiVersion: v1 +kind: Service +metadata: + name: createorreplace-it-resourcelist-service +spec: + selector: + app: MyApp + ports: + - protocol: TCP + port: 80 + targetPort: 9090 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: createorreplace-it-resourcelist-configmap + namespace: default +data: + allowed: '"true"' + enemies: aliens + updated: true + lives: "3" diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceListTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceListTest.java index c10184d5191..59b143bb61e 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceListTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceListTest.java @@ -155,8 +155,6 @@ void testCreateOrReplaceWithoutDeleteExisting() throws Exception { @Test void testCreateOrReplaceWithDeleteExisting() throws Exception { - server.expect().post().withPath("/api/v1/namespaces/ns1/services").andReturn(HTTP_CONFLICT, service).once(); - server.expect().post().withPath("/api/v1/namespaces/ns1/configmaps").andReturn(HTTP_CONFLICT, configMap).once(); server.expect().delete().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(HTTP_OK , service).once(); server.expect().delete().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(HTTP_OK, configMap).once(); server.expect().post().withPath("/api/v1/namespaces/ns1/services").andReturn(HTTP_OK, updatedService).once(); @@ -164,7 +162,7 @@ void testCreateOrReplaceWithDeleteExisting() throws Exception { client.resourceList(resourcesToUpdate).inNamespace("ns1").deletingExisting().createOrReplace(); - assertEquals(6, server.getMockServer().getRequestCount()); + assertEquals(4, server.getMockServer().getRequestCount()); RecordedRequest request = server.getLastRequest(); assertEquals("/api/v1/namespaces/ns1/configmaps", request.getPath()); assertEquals("POST", request.getMethod()); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceTest.java index ed59e3b46d6..4295567412b 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ResourceTest.java @@ -105,7 +105,6 @@ void testCreateWithExplicitNamespace() { void testCreateOrReplaceWithDeleteExisting() throws Exception { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); - server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, pod1).once(); 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(); @@ -114,7 +113,7 @@ void testCreateOrReplaceWithDeleteExisting() throws Exception { assertEquals(pod1, response); RecordedRequest request = server.getLastRequest(); - assertEquals(3, server.getMockServer().getRequestCount()); + assertEquals(2, server.getMockServer().getRequestCount()); assertEquals("/api/v1/namespaces/ns1/pods", request.getPath()); assertEquals("POST", request.getMethod()); }