diff --git a/CHANGELOG.md b/CHANGELOG.md index dd9dbb28267..2c12016ff7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,11 @@ #### Bugs #### Improvements +* Fix #2292: Update createOrReplace to do replace when create fails with conflict #### Dependency Upgrade * Fix #2355: bump jandex from 2.1.3.Final to 2.2.0.Final -* Fix #2353: bump workflow action-setup-* versions + kubernetes to 1.18.6 +* Fix #2353: bump workflow action-setup- versions + kubernetes to 1.18.6 #### New Features * Fix #2287: Add support for V1 and V1Beta1 CustomResourceDefinition diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/CreateOrReplaceable.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/CreateOrReplaceable.java index 1bea0977b16..9cc5924c1bf 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/CreateOrReplaceable.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/CreateOrReplaceable.java @@ -17,7 +17,20 @@ public interface CreateOrReplaceable { + /** + * Creates a provided resource in a Kubernetes Cluster. If creation + * fails with a HTTP_CONFLICT, it tries to replace resource. + * + * @param item item to create or replace + * @return created item returned in kubernetes api response + */ T createOrReplace(I... item); + /** + * Create or replace a resource in a Kubernetes Cluster dynamically with + * the help of Kubernetes Model Builders. + * + * @return created item returned in kubernetes api response + */ D createOrReplaceWithNew(); } 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 2ab34623c24..7e548d0535f 100644 --- 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 @@ -15,6 +15,8 @@ */ package io.fabric8.kubernetes.client.dsl.base; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.fabric8.kubernetes.client.utils.ResourceCompare; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -395,24 +397,40 @@ public D createOrReplaceWithNew() throws KubernetesClientException { @Override public T createOrReplace(T... items) { - T item = getItem(); + T itemToCreateOrReplace = getItem(); if (items.length > 1) { throw new IllegalArgumentException("Too many items to create."); } else if (items.length == 1) { - item = items[0]; + itemToCreateOrReplace = items[0]; } - if (item == null) { + if (itemToCreateOrReplace == null) { throw new IllegalArgumentException("Nothing to create."); } - if (Utils.isNullOrEmpty(name) && item instanceof HasMetadata) { - return withName(((HasMetadata)item).getMetadata().getName()).createOrReplace(item); + if (Utils.isNullOrEmpty(name)) { + + return withName(itemToCreateOrReplace.getMetadata().getName()).createOrReplace(itemToCreateOrReplace); } - if (fromServer().get() == null) { - return create(item); - } else { - return replace(item); + + try { + // Create + KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, null); + return create(itemToCreateOrReplace); + } catch (KubernetesClientException exception) { + if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { + throw exception; + } + + // Conflict; Do Replace + T itemFromServer = fromServer().get(); + if (ResourceCompare.equals(itemFromServer, itemToCreateOrReplace)) { + // Nothing changed, ignore + return itemToCreateOrReplace; + } else { + KubernetesResourceUtil.setResourceVersion(itemToCreateOrReplace, KubernetesResourceUtil.getResourceVersion(itemFromServer)); + return replace(itemToCreateOrReplace); + } } } 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 6e28f3b7bbf..6aca6b72a38 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,7 +17,10 @@ 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 org.slf4j.Logger; @@ -137,22 +140,33 @@ public HasMetadata apply() { public HasMetadata createOrReplace() { HasMetadata meta = acceptVisitors(asHasMetadata(item), visitors); ResourceHandler h = handlerOf(meta); - HasMetadata r = h.reload(client, config, meta.getMetadata().getNamespace(), meta); String namespaceToUse = meta.getMetadata().getNamespace(); - if (r == null) { + String resourceVersion = KubernetesResourceUtil.getResourceVersion(meta); + try { + // Create + KubernetesResourceUtil.setResourceVersion(meta, null); return h.create(client, config, namespaceToUse, meta); - } else if (deletingExisting) { - Boolean deleted = h.delete(client, config, namespaceToUse, propagationPolicy, meta); - if (!deleted) { - throw new KubernetesClientException("Failed to delete existing item:" + meta); + } catch (KubernetesClientException exception) { + if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { + throw exception; + } + + // Conflict; check deleteExisting flag otherwise replace + HasMetadata r = h.reload(client, config, meta.getMetadata().getNamespace(), meta); + 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 if (ResourceCompare.equals(r, meta)) { + LOGGER.debug("Item has not changed. Skipping"); + return meta; + } else { + KubernetesResourceUtil.setResourceVersion(meta, resourceVersion); + return h.replace(client, config, namespaceToUse, meta); } - return h.create(client, config, namespaceToUse, meta); - } else if (ResourceCompare.equals(r, meta)) { - LOGGER.debug("Item has not changed. Skipping"); - return meta; - } else { - return h.replace(client, config, namespaceToUse, meta); } } 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 b6855218894..5b5bc951e7e 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 @@ -36,12 +36,14 @@ 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.ResourceCompare; import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Utils; import io.fabric8.openshift.api.model.Parameter; import io.fabric8.openshift.api.model.Template; +import java.net.HttpURLConnection; import java.util.concurrent.RejectedExecutionException; import java.util.function.Predicate; import okhttp3.OkHttpClient; @@ -261,30 +263,31 @@ public List createOrReplace() { List result = new ArrayList<>(); for (HasMetadata meta : acceptVisitors(asHasMetadata(item, true), visitors)) { ResourceHandler h = handlerOf(meta); - HasMetadata r = h.reload(client, config, meta.getMetadata().getNamespace(), meta); String namespaceToUse = meta.getMetadata().getNamespace(); - if (r == null) { - HasMetadata created = h.create(client, config, namespaceToUse, meta); - if (created != null) { - result.add(created); - } - } else if(deletingExisting) { - Boolean deleted = h.delete(client, config, namespaceToUse, propagationPolicy, meta); - if (!deleted) { - throw new KubernetesClientException("Failed to delete existing item:" + meta); + 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; } - HasMetadata created = h.create(client, config, namespaceToUse, meta); - if (created != null) { - result.add(created); - } - } else if (ResourceCompare.equals(r, meta)) { - LOGGER.debug("Item has not changed. Skipping"); - } else { - HasMetadata replaced = h.replace(client, config, namespaceToUse, meta); - if (replaced != null) { - result.add(replaced); + // Conflict; check deleteExisting flag otherwise replace + HasMetadata r = h.reload(client, config, meta.getMetadata().getNamespace(), meta); + 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 if (ResourceCompare.equals(r, meta)) { + LOGGER.debug("Item has not changed. Skipping"); + } else { + KubernetesResourceUtil.setResourceVersion(meta, resourceVersion); + result.add(h.replace(client, config, namespaceToUse, meta)); } } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java index ce2d28e8d4e..06b4fc99202 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/KubernetesResourceUtil.java @@ -55,6 +55,21 @@ public static String getResourceVersion(HasMetadata entity) { return null; } + /** + * Set resource version of a kubernetes resource + * + * @param entity entity provided + * @param resourceVersion updated resource version + */ + public static void setResourceVersion(HasMetadata entity, String resourceVersion) { + if (entity != null) { + ObjectMeta metadata = entity.getMetadata(); + if (metadata != null) { + metadata.setResourceVersion(resourceVersion); + } + } + } + /** * Returns the kind of the entity * diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/ResourceCompare.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/ResourceCompare.java index e542b9a179f..519ecb205e3 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/ResourceCompare.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/ResourceCompare.java @@ -18,45 +18,116 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import io.fabric8.kubernetes.api.model.KubernetesList; -import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; public class ResourceCompare { + private ResourceCompare() {} - private static TypeReference> TYPE_REF = new TypeReference>(){}; + private static TypeReference> TYPE_REF = new TypeReference>(){}; - private static final String METADATA = "metadata"; - private static final String STATUS = "status"; - private static final String LABELS = "labels"; + private static final String METADATA = "metadata"; + private static final String SPEC = "spec"; + private static final String ITEMS = "items"; - public static boolean equals(T left, T right) { - ObjectMapper jsonMapper = Serialization.jsonMapper(); - Map leftJson = (Map) jsonMapper.convertValue(left, TYPE_REF); - Map rightJson = (Map) jsonMapper.convertValue(right, TYPE_REF); + /** + * This method returns true when left Kubernetes resource contains + * all data that's present in right Kubernetes resource, this method + * won't consider fields that are missing in right parameters. Values + * which are present in right would only be compared. + * + * @param left kubernetes resource (fetched from cluster) + * @param right kubernetes resource (provided as input by user) + * @param type for kubernetes resource + * + * @return boolean value whether both resources are actually equal or not + */ + public static boolean equals(T left, T right) { + ObjectMapper jsonMapper = Serialization.jsonMapper(); + Map leftJson = jsonMapper.convertValue(left, TYPE_REF); + Map rightJson = jsonMapper.convertValue(right, TYPE_REF); - Map leftLabels = fetchLabels(leftJson); - Map rightLabels = fetchLabels(rightJson); + if (left instanceof KubernetesList) { + return compareKubernetesList(leftJson, rightJson); + } else { + return compareKubernetesResource(leftJson, rightJson); + } + } + + public static boolean compareKubernetesList(Map leftJson, Map rightJson) { + List> leftItems = (List>)leftJson.get(ITEMS); + List> rightItems = (List>)rightJson.get(ITEMS); + + if (leftItems != null && rightItems != null) { + if (leftItems.size() != rightItems.size()) { + return false; + } + + for (int i = 0; i < rightItems.size(); i++) { + if (!compareKubernetesResource(leftItems.get(i), rightItems.get(i))) { + return false; + } + } + } else return leftItems != null; + return true; + } + + public static boolean compareKubernetesResource(Map leftJson, Map rightJson) { + return isEqualMetadata(leftJson, rightJson) && + isEqualSpec(leftJson, rightJson); + } - HashMap leftMap = trim(leftJson); - HashMap rightMap = trim(rightJson); + private static boolean isEqualMetadata(Map leftMap, Map rightMap) { + Map leftMetadata = (Map) leftMap.get(METADATA); + Map rightMetadata = (Map) rightMap.get(METADATA); - return leftMap.equals(rightMap) && leftLabels.equals(rightLabels); + if (leftMetadata == null && rightMetadata == null) { + return true; + } else if (leftMetadata != null && rightMetadata == null) { + return true; + } else if (leftMetadata == null) { + return false; } - private static HashMap trim(Map map) { - HashMap result = new HashMap<>(map); - result.remove(STATUS); - result.remove(METADATA); - return result; + return isLeftMapSupersetOfRight(leftMetadata, rightMetadata); + } + + private static boolean isEqualSpec(Map leftMap, Map rightMap) { + Map leftSpec = (Map) leftMap.get(SPEC); + Map rightSpec = (Map) rightMap.get(SPEC); + + if (leftSpec == null && rightSpec == null) { + return true; + } else if (leftSpec != null && rightSpec == null) { + return true; + } else if (leftSpec == null) { + return false; } - private static Map fetchLabels(Map map){ - if (!map.containsKey(METADATA) || !((Map)map.get(METADATA)).containsKey(LABELS)){ - return Collections.emptyMap(); - } - return (Map) ((Map)map.get(METADATA)).get(LABELS); + return isLeftMapSupersetOfRight(leftSpec, rightSpec); + } + + /** + * Iterates via keys of right map to see if they are present in left map + * + * @param leftMap a hashmap of string, object + * @param rightMap a hashmap of string, object + * @return boolean value indicating whether left contains all keys and values of right map or not + */ + private static boolean isLeftMapSupersetOfRight(Map leftMap, Map rightMap) { + for (Map.Entry entry : rightMap.entrySet()) { + if (!leftMap.containsKey(entry.getKey())) { + return false; + } + + if (!leftMap.get(entry.getKey()).equals(entry.getValue())) { + return false; + } } + return true; + } } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java index a13a3705085..4fe9f101faa 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/internal/KubernetesResourceUtilTest.java @@ -20,8 +20,11 @@ import io.fabric8.kubernetes.api.model.ConfigMapBuilder; import io.fabric8.kubernetes.api.model.Event; import io.fabric8.kubernetes.api.model.EventBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -112,4 +115,34 @@ void testSortEventListBasedOnTimestamp() { assertEquals("event2", eventList.get(1).getMetadata().getName()); assertEquals("event1", eventList.get(2).getMetadata().getName()); } + + @Test + @DisplayName("Should be able to get resource version") + void testGetResourceVersion() { + // Given + Pod pod = new PodBuilder() + .withNewMetadata().withName("test").withResourceVersion("1001").endMetadata() + .build(); + + // When + String resourceVersion = KubernetesResourceUtil.getResourceVersion(pod); + + // Then + assertEquals("1001", resourceVersion); + } + + @Test + @DisplayName("Should be able to update resource version") + void testSetResourceVersion() { + // Given + Pod pod = new PodBuilder() + .withNewMetadata().withName("test").withResourceVersion("1001").endMetadata() + .build(); + + // When + KubernetesResourceUtil.setResourceVersion(pod, "1002"); + + // Then + assertEquals("1002", pod.getMetadata().getResourceVersion()); + } } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/ResourceCompareTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/ResourceCompareTest.java index 0eff43853f0..89d2b844902 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/ResourceCompareTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/utils/ResourceCompareTest.java @@ -17,7 +17,9 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; +import io.fabric8.kubernetes.api.model.IntOrString; import io.fabric8.kubernetes.api.model.KubernetesList; import io.fabric8.kubernetes.api.model.KubernetesListBuilder; import io.fabric8.kubernetes.api.model.Pod; @@ -52,12 +54,12 @@ public void setup() { } @Test - public void testResourceCompareEqualsTrue() throws Exception { + void testResourceCompareEqualsTrue() { assertThat(ResourceCompare.equals(kubeList, kubeList), is(true)); } @Test - public void testResourceCompareEqualsFalse() throws Exception { + void testResourceCompareEqualsFalse() { final ReplicationController rc = new ReplicationControllerBuilder() .withNewMetadata().withName("repl1").withNamespace("test").endMetadata() .withNewSpec().withReplicas(2).endSpec() @@ -68,20 +70,64 @@ public void testResourceCompareEqualsFalse() throws Exception { } @Test - public void testPodResourceCompareEqualsFalseNoLabels() throws Exception { + void testPodResourceCompareEqualsTrueNoLabels() { Pod podNoLabels = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); - assertThat(ResourceCompare.equals(pod, podNoLabels), is(false)); + assertThat(ResourceCompare.equals(pod, podNoLabels), is(true)); } @Test - public void testPodResourceCompareEqualsTrueMatchingLabels() throws Exception { + void testPodResourceCompareEqualsTrueMatchingLabels() { Pod podWithLabels = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").withLabels(Collections.singletonMap("label", "value")).and().build(); assertThat(ResourceCompare.equals(pod, podWithLabels), is(true)); } @Test - public void testPodResourceCompareEqualsFalseDifferentLabels() throws Exception { + void testPodResourceCompareEqualsFalseDifferentLabels() { Pod podWithLabels = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").withLabels(Collections.singletonMap("label", "another value")).and().build(); assertThat(ResourceCompare.equals(pod, podWithLabels), is(false)); } + + @Test + void testServiceDifferenceFromClusterAndAsObject() { + // Given + Service serviceFromCluster = new ServiceBuilder() + .withNewMetadata() + .withCreationTimestamp("2020-07-27T10:36:33Z") + .withName("my-service") + .withNamespace("default") + .withResourceVersion("202998") + .withSelfLink("/api/v1/namespaces/default/services/my-service") + .withUid("99fe3964-b53b-473f-b1d8-bdb0390d1634") + .endMetadata() + .withNewSpec() + .withClusterIP("10.110.153.70") + .addNewPort() + .withPort(80) + .withProtocol("TCP") + .withTargetPort(new IntOrString("9376")) + .endPort() + .addToSelector(Collections.singletonMap("app", "MyApp")) + .withType("ClusterIP") + .endSpec() + .build(); + + Service serviceAsObj = new ServiceBuilder() + .withNewMetadata().withName("my-service").endMetadata() + .withNewSpec() + .addToSelector(Collections.singletonMap("app", "MyApp")) + .addNewPort() + .withPort(80) + .withProtocol("TCP") + .withTargetPort(new IntOrString("9376")) + .endPort() + .endSpec() + .build(); + + // When + boolean result = ResourceCompare.equals(serviceFromCluster, serviceAsObj); + + // Then + assertTrue(result); + } + } diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ResourceIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ResourceIT.java index e1fe35c68b3..2ccbb24cdd2 100644 --- a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ResourceIT.java +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ResourceIT.java @@ -130,7 +130,6 @@ public void createOrReplace() { client.resourceList(list).inNamespace(session.getNamespace()).createOrReplace(); // Modify - service = client.services().inNamespace(session.getNamespace()).withName("my-service").get(); service.getSpec().getPorts().get(0).setTargetPort(new IntOrString(9998)); configMap.getData().put("test", "createOrReplace"); configMap.getData().put("io", "fabric8"); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CreateOrReplaceResourceTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CreateOrReplaceResourceTest.java index 4eb2b5544a8..4e0ffe8fa6f 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CreateOrReplaceResourceTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CreateOrReplaceResourceTest.java @@ -19,22 +19,27 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.DoneablePod; import io.fabric8.kubernetes.api.model.HasMetadata; 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.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicable; +import io.fabric8.kubernetes.client.dsl.internal.NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Ignore; import org.junit.Rule; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import java.net.HttpURLConnection; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; @EnableRuleMigrationSupport public class CreateOrReplaceResourceTest { @@ -43,11 +48,15 @@ public class CreateOrReplaceResourceTest { public KubernetesServer server = new KubernetesServer(); @Test - public void testResourceReplace() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(200, new PodBuilder() + @DisplayName("Should replace an existing resource in Kubernetes Cluster") + void testResourceReplace() { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).always(); - server.expect().put().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(200, new PodBuilder() + server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder() + .withNewMetadata().withResourceVersion("12345").and().build()).times(2); + + server.expect().put().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -57,10 +66,9 @@ public void testResourceReplace() throws Exception { } @Test - public void testResourceCreate() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(404, new StatusBuilder().build()).always(); - - server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(201, new PodBuilder() + @DisplayName("Should create a new resource in Kubernetes Cluster") + void testResourceCreate() { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CREATED, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -70,10 +78,22 @@ public void testResourceCreate() throws Exception { } @Test - public void testCreate() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(404, new StatusBuilder().build()).always(); + @DisplayName("Should throw Exception on failed create") + void testResourceCreateFailure() { + // Given + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_BAD_REQUEST, new PodBuilder() + .withNewMetadata().withResourceVersion("12345").endMetadata().build()).once(); + KubernetesClient client = server.getClient(); + NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicable podOperation = client.resource(new PodBuilder().withNewMetadata().withName("pod123").endMetadata().build()); - server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(201, new PodBuilder() + // When + assertThrows(KubernetesClientException.class, podOperation::createOrReplace); + } + + @Test + @DisplayName("Should create a new resource in Kubernetes Cluster") + void testCreate() { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CREATED, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -83,10 +103,12 @@ public void testCreate() throws Exception { } @Test - public void testReplace() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(200, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).always(); + @DisplayName("Shoulc replace an existing resource in Kubernetes Cluster") + void testReplace() { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).always(); - server.expect().put().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(200, new PodBuilder() + server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).times(2); + server.expect().put().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -96,10 +118,24 @@ public void testReplace() throws Exception { } @Test - public void testResourceCreateFromLoad() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(404, new StatusBuilder().build()).always(); + @DisplayName("Should throw exception on failed replace") + void testFailedReplace() { + // Given + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).always(); + server.expect().get().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).times(2); + server.expect().put().withPath("/api/v1/namespaces/test/pods/pod123").andReturn(HttpURLConnection.HTTP_BAD_REQUEST, new PodBuilder() + .withNewMetadata().withResourceVersion("12345").and().build()).once(); + KubernetesClient client = server.getClient(); + DoneablePod podOperation = client.pods().createOrReplaceWithNew(); + + // When + assertThrows(KubernetesClientException.class, () -> podOperation.withNewMetadata().withName("pod123").and().withNewSpec().and().done()); + } - server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(201, new PodBuilder() + @Test + @DisplayName("Should create a new resource in Kubernetes Cluster") + void testResourceCreateFromLoad() throws Exception { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CREATED, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -110,18 +146,18 @@ public void testResourceCreateFromLoad() throws Exception { assertEquals("12345", pod.getMetadata().getResourceVersion()); RecordedRequest request = server.getMockServer().takeRequest(); - assertEquals("/api/v1/namespaces/test/pods/nginx", request.getPath()); - - request = server.getMockServer().takeRequest(); + assertEquals("/api/v1/namespaces/test/pods", request.getPath()); Pod requestPod = new ObjectMapper().readerFor(Pod.class).readValue(request.getBody().inputStream()); assertEquals("nginx", requestPod.getMetadata().getName()); } @Test - public void testResourceReplaceFromLoad() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(200, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).always(); + @DisplayName("Should replace an existing resource in Kubernetes Cluster") + void testResourceReplaceFromLoad() throws Exception { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).always(); - server.expect().put().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(200, new PodBuilder() + server.expect().get().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder().withNewMetadata().withResourceVersion("12345").and().build()).times(2); + server.expect().put().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -138,10 +174,9 @@ public void testResourceReplaceFromLoad() throws Exception { } @Test - public void testCreateFromLoad() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(404, new StatusBuilder().build()).always(); - - server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(201, new PodBuilder() + @DisplayName("Should create a new resource from yaml") + void testCreateFromLoad() throws Exception { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CREATED, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -155,10 +190,12 @@ public void testCreateFromLoad() throws Exception { } @Test - public void testReplaceFromLoad() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(200, new PodBuilder().build()).always(); + @DisplayName("Should update existing resource") + void testReplaceFromLoad() throws Exception { + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, new PodBuilder().build()).always(); - server.expect().put().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(200, new PodBuilder() + server.expect().get().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder().build()).times(2); + server.expect().put().withPath("/api/v1/namespaces/test/pods/nginx").andReturn(HttpURLConnection.HTTP_OK, new PodBuilder() .withNewMetadata().withResourceVersion("12345").and().build()).once(); KubernetesClient client = server.getClient(); @@ -174,11 +211,12 @@ public void testReplaceFromLoad() throws Exception { } @Test - public void testReplaceWithoutLock() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/test/configmaps/map1").andReturn(200, new ConfigMapBuilder() + @DisplayName("Should replace an existing ConfigMap without lock") + void testReplaceWithoutLock() throws Exception { + server.expect().get().withPath("/api/v1/namespaces/test/configmaps/map1").andReturn(HttpURLConnection.HTTP_OK, new ConfigMapBuilder() .withNewMetadata().withResourceVersion("1000").and().build()).always(); - server.expect().put().withPath("/api/v1/namespaces/test/configmaps/map1").andReturn(200, new ConfigMapBuilder() + server.expect().put().withPath("/api/v1/namespaces/test/configmaps/map1").andReturn(HttpURLConnection.HTTP_OK, new ConfigMapBuilder() .withNewMetadata().withResourceVersion("1001").and().build()).once(); KubernetesClient client = server.getClient(); @@ -193,8 +231,9 @@ public void testReplaceWithoutLock() throws Exception { } @Test - public void testReplaceWithLock() throws Exception { - server.expect().put().withPath("/api/v1/namespaces/test/configmaps/map1").andReturn(200, new ConfigMapBuilder() + @DisplayName("Should replace an existing resource with lock") + void testReplaceWithLock() throws Exception { + server.expect().put().withPath("/api/v1/namespaces/test/configmaps/map1").andReturn(HttpURLConnection.HTTP_OK, new ConfigMapBuilder() .withNewMetadata().withResourceVersion("1001").and().build()).once(); KubernetesClient client = server.getClient(); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/JobTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/JobTest.java index 902adefd93e..936b1971a6c 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/JobTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/JobTest.java @@ -16,7 +16,6 @@ package io.fabric8.kubernetes.client.mock; -import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; @@ -242,7 +241,6 @@ public void testDeleteWithNamespaceMismatch() { public void testCreateWithNameMismatch() { Assertions.assertThrows(KubernetesClientException.class, () -> { Job job1 = new JobBuilder().withNewMetadata().withName("job1").withNamespace("test").and().build(); - Job job2 = new JobBuilder().withNewMetadata().withName("job2").withNamespace("ns1").and().build(); KubernetesClient client = server.getClient(); client.batch().jobs().inNamespace("test1").withName("myjob1").create(job1); @@ -265,9 +263,11 @@ public void testCreateOrReplaceWithExistingJob() { .build(); server.expect().get().withPath("/apis/batch/v1/namespaces/test/jobs/job1") - .andReturn(200, jobExistingInServer).always(); + .andReturn(HttpURLConnection.HTTP_OK, jobExistingInServer).always(); + server.expect().post().withPath("/apis/batch/v1/namespaces/test/jobs") + .andReturn(HttpURLConnection.HTTP_CONFLICT, jobExistingInServer).once(); server.expect().put().withPath("/apis/batch/v1/namespaces/test/jobs/job1") - .andReturn(200, getJobBuilder() + .andReturn(HttpURLConnection.HTTP_OK, getJobBuilder() .editOrNewMetadata().addToLabels("foo", "bar").addToLabels("foo1", "bar1").endMetadata() .editSpec() .editOrNewTemplate().editOrNewMetadata() diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/NetworkingV1beta1IngressTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/NetworkingV1beta1IngressTest.java index d4abfe7edf7..7649bfdf4f3 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/NetworkingV1beta1IngressTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/NetworkingV1beta1IngressTest.java @@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import java.net.HttpURLConnection; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -188,4 +189,28 @@ public void testCreateWithNameMismatch() { }); } + @Test + void testCreateOrReplaceWhenAnnotationUpdated() { + // Given + Ingress ingressFromServer = new IngressBuilder().withNewMetadata().withName("ing1").endMetadata().build(); + Ingress ingressUpdated = new IngressBuilder(ingressFromServer).editOrNewMetadata() + .addToAnnotations("nginx.ingress.kubernetes.io/rewrite-target", "/") + .endMetadata().build(); + server.expect().post().withPath("/apis/networking.k8s.io/v1beta1/namespaces/ns1/ingresses") + .andReturn(HttpURLConnection.HTTP_CONFLICT, ingressFromServer).once(); + server.expect().get().withPath("/apis/networking.k8s.io/v1beta1/namespaces/ns1/ingresses/ing1") + .andReturn(HttpURLConnection.HTTP_OK, ingressFromServer).times(2); + server.expect().put().withPath("/apis/networking.k8s.io/v1beta1/namespaces/ns1/ingresses/ing1") + .andReturn(HttpURLConnection.HTTP_OK, ingressUpdated).once(); + KubernetesClient client = server.getClient(); + + // When + ingressUpdated = client.network().ingresses().inNamespace("ns1").createOrReplace(ingressUpdated); + + // Then + assertNotNull(ingressUpdated); + assertNotNull(ingressUpdated.getMetadata()); + assertTrue(ingressUpdated.getMetadata().getAnnotations().containsKey("nginx.ingress.kubernetes.io/rewrite-target")); + } + } diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PersistentVolumeClaimTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PersistentVolumeClaimTest.java index 96fd4f0ce73..ed55179b4c6 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PersistentVolumeClaimTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PersistentVolumeClaimTest.java @@ -33,6 +33,9 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import java.net.HttpURLConnection; +import java.util.Collections; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -174,4 +177,49 @@ void testBuild() { assertNotNull(persistentVolumeClaim); } + @Test + void testCreateOrReplaceIgnoreWhenNoChange() { + // Given + PersistentVolumeClaim pvcOrignal = new PersistentVolumeClaimBuilder() + .withNewMetadata().withName("my-pvc").endMetadata() + .withNewSpec() + .withAccessModes("ReadWriteOnce") + .withNewResources() + .withRequests(Collections.singletonMap("storage", new Quantity("20Gi"))) + .endResources() + .endSpec() + .build(); + PersistentVolumeClaim pvcFromServer = new PersistentVolumeClaimBuilder() + .withNewMetadata() + .withName("my-pvc") + .withCreationTimestamp("2020-07-27T11:02:15Z") + .addToFinalizers("kubernetes.io/pvc-protection") + .withNamespace("default") + .withResourceVersion("203697") + .withSelfLink("/api/v1/namespaces/default/persistentvolumeclaims/my-pvc") + .withUid("60817eaa-19d8-41ba-adb4-3ea75860e1f8") + .endMetadata() + .withNewSpec() + .withAccessModes("ReadWriteOnce") + .withNewResources() + .withRequests(Collections.singletonMap("storage", new Quantity("20Gi"))) + .endResources() + .withStorageClassName("standard") + .withVolumeMode("Filesystem") + .withVolumeName("pvc-60817eaa-19d8-41ba-adb4-3ea75860e1f8") + .endSpec() + .build(); + server.expect().post().withPath("/api/v1/namespaces/ns1/persistentvolumeclaims") + .andReturn(HttpURLConnection.HTTP_CONFLICT, pvcFromServer).once(); + server.expect().get().withPath("/api/v1/namespaces/ns1/persistentvolumeclaims/my-pvc") + .andReturn(HttpURLConnection.HTTP_OK, pvcFromServer).once(); + KubernetesClient client = server.getClient(); + + // When + PersistentVolumeClaim pvcResult = client.persistentVolumeClaims().inNamespace("ns1").createOrReplace(pvcOrignal); + + // Then + assertNotNull(pvcResult); + } + } 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 8911938411f..c11a55845a1 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 @@ -28,6 +28,8 @@ import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.ServiceBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import okhttp3.mockwebserver.RecordedRequest; @@ -35,10 +37,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import java.net.HttpURLConnection; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -73,11 +77,10 @@ public class ResourceListTest { @Test - public void testCreateOrReplace() { + void testCreateOrReplace() { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); - server.expect().get().withPath("/api/v1/namespaces/test/pods/pod1").andReturn(404, "").once(); - server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(201, pod1).once(); + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CREATED, pod1).once(); KubernetesClient client = server.getClient(); List response = client.resourceList(new PodListBuilder().addToItems(pod1).build()).createOrReplace(); @@ -85,11 +88,22 @@ public void testCreateOrReplace() { } @Test - public void testCreateWithExplicitNamespace() { + void testCreateOrReplaceFailedCreate() { + // Given + Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_UNAVAILABLE, pod1).once(); + KubernetesClient client = server.getClient(); + NamespaceListVisitFromServerGetDeleteRecreateWaitApplicable listOp = client.resourceList(new PodListBuilder().addToItems(pod1).build()); + + // When + assertThrows(KubernetesClientException.class, listOp::createOrReplace); + } + + @Test + void testCreateWithExplicitNamespace() { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); - server.expect().get().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(404, "").once(); - server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(201, pod1).once(); + server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(HttpURLConnection.HTTP_CREATED, pod1).once(); KubernetesClient client = server.getClient(); List response = client.resourceList(new PodListBuilder().addToItems(pod1).build()).inNamespace("ns1").apply(); @@ -97,15 +111,15 @@ public void testCreateWithExplicitNamespace() { } @Test - public void testDelete() { + void testDelete() { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); Pod pod2 = new PodBuilder().withNewMetadata().withName("pod2").withNamespace("ns1").and().build(); Pod pod3 = new PodBuilder().withNewMetadata().withName("pod3").withNamespace("any").and().build(); - server.expect().withPath("/api/v1/namespaces/test/pods/pod1").andReturn(200, pod1).times(2); - server.expect().withPath("/api/v1/namespaces/ns1/pods/pod2").andReturn(200, pod2).times(2); - server.expect().withPath("/api/v1/namespaces/any/pods/pod3").andReturn(200, pod3).times(1); + server.expect().withPath("/api/v1/namespaces/test/pods/pod1").andReturn(HttpURLConnection.HTTP_OK, pod1).times(2); + server.expect().withPath("/api/v1/namespaces/ns1/pods/pod2").andReturn(HttpURLConnection.HTTP_OK, pod2).times(2); + server.expect().withPath("/api/v1/namespaces/any/pods/pod3").andReturn(HttpURLConnection.HTTP_OK, pod3).times(1); KubernetesClient client = server.getClient(); @@ -119,37 +133,41 @@ public void testDelete() { } @Test - public void testCreateOrReplaceWithoutDeleteExisting() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(200 , service).times(2); - server.expect().get().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(200, configMap).times(2); - server.expect().put().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(200, updatedService).once(); - server.expect().put().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(200, updatedConfigMap).once(); + void testCreateOrReplaceWithoutDeleteExisting() throws Exception { + server.expect().post().withPath("/api/v1/namespaces/ns1/services").andReturn(HttpURLConnection.HTTP_CONFLICT, service).once(); + server.expect().post().withPath("/api/v1/namespaces/ns1/configmaps").andReturn(HttpURLConnection.HTTP_CONFLICT, configMap).once(); + server.expect().get().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(HttpURLConnection.HTTP_OK , service).times(2); + server.expect().get().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(HttpURLConnection.HTTP_OK, configMap).times(2); + server.expect().put().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(HttpURLConnection.HTTP_OK, updatedService).once(); + server.expect().put().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(HttpURLConnection.HTTP_OK, updatedConfigMap).once(); KubernetesClient client = server.getClient(); KubernetesList list = new KubernetesListBuilder().withItems(updatedService, updatedConfigMap).build(); client.resourceList(list).inNamespace("ns1").createOrReplace(); - assertEquals(6, server.getMockServer().getRequestCount()); + assertEquals(8, server.getMockServer().getRequestCount()); RecordedRequest request = server.getLastRequest(); assertEquals("/api/v1/namespaces/ns1/configmaps/my-configmap", request.getPath()); assertEquals("PUT", request.getMethod()); } @Test - public void testCreateOrReplaceWithDeleteExisting() throws Exception { - server.expect().get().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(200, service).once(); - server.expect().get().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(200, configMap).once(); - server.expect().delete().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(200 , service).once(); - server.expect().delete().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(200, configMap).once(); - server.expect().post().withPath("/api/v1/namespaces/ns1/services").andReturn(200, updatedService).once(); - server.expect().post().withPath("/api/v1/namespaces/ns1/configmaps").andReturn(200, updatedConfigMap).once(); + void testCreateOrReplaceWithDeleteExisting() throws Exception { + server.expect().post().withPath("/api/v1/namespaces/ns1/services").andReturn(HttpURLConnection.HTTP_CONFLICT, service).once(); + server.expect().post().withPath("/api/v1/namespaces/ns1/configmaps").andReturn(HttpURLConnection.HTTP_CONFLICT, configMap).once(); + server.expect().get().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(HttpURLConnection.HTTP_OK , service).once(); + server.expect().get().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(HttpURLConnection.HTTP_OK, configMap).once(); + server.expect().delete().withPath("/api/v1/namespaces/ns1/services/my-service").andReturn(HttpURLConnection.HTTP_OK , service).once(); + server.expect().delete().withPath("/api/v1/namespaces/ns1/configmaps/my-configmap").andReturn(HttpURLConnection.HTTP_OK, configMap).once(); + server.expect().post().withPath("/api/v1/namespaces/ns1/services").andReturn(HttpURLConnection.HTTP_OK, updatedService).once(); + server.expect().post().withPath("/api/v1/namespaces/ns1/configmaps").andReturn(HttpURLConnection.HTTP_OK, updatedConfigMap).once(); KubernetesClient client = server.getClient(); KubernetesList list = new KubernetesListBuilder().withItems(updatedService, updatedConfigMap).build(); client.resourceList(list).inNamespace("ns1").deletingExisting().createOrReplace(); - assertEquals(6, server.getMockServer().getRequestCount()); + assertEquals(8, 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 9b2ba5c20c4..3996d628d98 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 @@ -16,9 +16,13 @@ package io.fabric8.kubernetes.client.mock; +import io.fabric8.kubernetes.api.model.DoneablePod; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.api.model.StatusBuilder; import io.fabric8.kubernetes.client.ResourceNotFoundException; +import io.fabric8.kubernetes.client.dsl.Applicable; +import io.fabric8.kubernetes.client.dsl.NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicable; +import io.fabric8.kubernetes.client.dsl.PodResource; import io.fabric8.kubernetes.client.dsl.base.WaitForConditionWatcher; import okhttp3.mockwebserver.RecordedRequest; import org.junit.Assert; @@ -47,6 +51,7 @@ import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.tuple; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -58,62 +63,87 @@ public class ResourceTest { public KubernetesServer server = new KubernetesServer(); - @Test - public void testCreateOrReplace() { - Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); + @Test + void testCreateOrReplace() { + // Given + Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_CREATED, pod1).once(); + KubernetesClient client = server.getClient(); - server.expect().get().withPath("/api/v1/namespaces/test/pods/pod1").andReturn(404, "").once(); - server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(201, pod1).once(); + // When + HasMetadata response = client.resource(pod1).createOrReplace(); - KubernetesClient client = server.getClient(); - HasMetadata response = client.resource(pod1).createOrReplace(); - assertEquals(pod1, response); - } + // Then + assertEquals(pod1, response); + } + + @Test + void testCreateOrReplaceWhenCreateFails() { + // Given + Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); + server.expect().post().withPath("/api/v1/namespaces/test/pods").andReturn(HttpURLConnection.HTTP_BAD_REQUEST, pod1).once(); + KubernetesClient client = server.getClient(); + NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicable podOperation = client.resource(pod1); + + // When + assertThrows(KubernetesClientException.class, podOperation::createOrReplace); + } @Test - public void testCreateWithExplicitNamespace() { + void testCreateWithExplicitNamespace() { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); - server.expect().get().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(404, "").once(); - server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(201, 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").apply(); + HasMetadata response = client.resource(pod1).inNamespace("ns1").createOrReplace(); assertEquals(pod1, response); } @Test - public void testCreateOrReplaceWithDeleteExisting() throws Exception { + void testCreateOrReplaceWithDeleteExisting() throws Exception { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); - server.expect().get().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(200, pod1).once(); - server.expect().delete().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(200, pod1).once(); - server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(201, pod1).once(); + server.expect().post().withPath("/api/v1/namespaces/ns1/pods").andReturn(HttpURLConnection.HTTP_CONFLICT, pod1).once(); + server.expect().get().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(HttpURLConnection.HTTP_OK, 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(); KubernetesClient client = server.getClient(); HasMetadata response = client.resource(pod1).inNamespace("ns1").deletingExisting().createOrReplace(); assertEquals(pod1, response); RecordedRequest request = server.getLastRequest(); - assertEquals(3, server.getMockServer().getRequestCount()); + assertEquals(4, server.getMockServer().getRequestCount()); assertEquals("/api/v1/namespaces/ns1/pods", request.getPath()); assertEquals("POST", request.getMethod()); } - @Test - public void testRequire() { - Assertions.assertThrows(ResourceNotFoundException.class, () -> { - Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test1").and().build(); + @Test + void testCreateOrReplaceWithDeleteExistingWithCreateFailed() { + // Given + 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_BAD_REQUEST, pod1).once(); + KubernetesClient client = server.getClient(); + Applicable podOperation = client.resource(pod1).inNamespace("ns1").deletingExisting(); - server.expect().get().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(HttpURLConnection.HTTP_NOT_FOUND, "").once(); + // When + assertThrows(KubernetesClientException.class, podOperation::createOrReplace); + } - KubernetesClient client = server.getClient(); - client.pods().inNamespace("ns1").withName("pod1").require(); - }); + @Test + void testRequire() { + server.expect().get().withPath("/api/v1/namespaces/ns1/pods/pod1").andReturn(HttpURLConnection.HTTP_NOT_FOUND, "").once(); + KubernetesClient client = server.getClient(); + PodResource podOp = client.pods().inNamespace("ns1").withName("pod1"); + + Assertions.assertThrows(ResourceNotFoundException.class, podOp::require); } @Test - public void testDelete() { + void testDelete() { Pod pod1 = new PodBuilder().withNewMetadata().withName("pod1").withNamespace("test").and().build(); Pod pod2 = new PodBuilder().withNewMetadata().withName("pod2").withNamespace("ns1").and().build(); Pod pod3 = new PodBuilder().withNewMetadata().withName("pod3").withNamespace("any").and().build(); @@ -133,7 +163,7 @@ public void testDelete() { @Test - public void testWatch() throws InterruptedException { + void testWatch() throws InterruptedException { Pod pod1 = new PodBuilder().withNewMetadata() .withName("pod1") .withResourceVersion("1") @@ -168,7 +198,7 @@ public void onClose(KubernetesClientException cause) { @Test - public void testWaitUntilReady() throws InterruptedException { + void testWaitUntilReady() throws InterruptedException { Pod pod1 = new PodBuilder().withNewMetadata() .withName("pod1") .withResourceVersion("1") @@ -205,7 +235,7 @@ public void testWaitUntilReady() throws InterruptedException { } @Test - public void testWaitUntilExistsThenReady() throws InterruptedException { + void testWaitUntilExistsThenReady() throws InterruptedException { Pod pod1 = new PodBuilder().withNewMetadata() .withName("pod1") .withResourceVersion("1") @@ -246,7 +276,7 @@ public void testWaitUntilExistsThenReady() throws InterruptedException { } @Test - public void testWaitUntilCondition() throws InterruptedException { + void testWaitUntilCondition() throws InterruptedException { Pod pod1 = new PodBuilder().withNewMetadata() .withName("pod1") .withResourceVersion("1") @@ -502,7 +532,7 @@ void testWaitOnConditionDeleted() throws InterruptedException { } @Test - public void testCreateAndWaitUntilReady() throws InterruptedException { + void testCreateAndWaitUntilReady() throws InterruptedException { Pod pod1 = new PodBuilder().withNewMetadata() .withName("pod1") .withResourceVersion("1") @@ -542,7 +572,7 @@ public void testCreateAndWaitUntilReady() throws InterruptedException { } @Test - public void testFromServerGet() { + void testFromServerGet() { Pod pod = new PodBuilder().withNewMetadata() .withName("pod1") .withNamespace("test") diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ServiceTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ServiceTest.java index af3408d5104..f1cfc32f9ea 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ServiceTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/ServiceTest.java @@ -84,25 +84,34 @@ void testCreate() { void testReplace() throws InterruptedException { Service serviceFromServer = new ServiceBuilder(service) .editOrNewSpec().withClusterIP("10.96.129.1").endSpec().build(); + Service serviceUpdated = new ServiceBuilder(service) + .editOrNewSpec().withClusterIP("10.96.129.1").endSpec() + .editMetadata().addToAnnotations("foo", "bar").endMetadata() + .build(); server.expect().get() .withPath("/api/v1/namespaces/test/services/httpbin") - .andReturn(200, serviceFromServer) + .andReturn(HttpURLConnection.HTTP_OK, serviceFromServer) .times(3); + server.expect().post() + .withPath("/api/v1/namespaces/test/services") + .andReturn(HttpURLConnection.HTTP_CONFLICT, serviceFromServer) + .once(); + server.expect().put() .withPath("/api/v1/namespaces/test/services/httpbin") - .andReturn(200, serviceFromServer) + .andReturn(HttpURLConnection.HTTP_OK, serviceUpdated) .once(); KubernetesClient client = server.getClient(); - Service responseSvc = client.services().inNamespace("test").createOrReplace(service); + Service responseSvc = client.services().inNamespace("test").createOrReplace(serviceUpdated); assertNotNull(responseSvc); assertEquals("httpbin", responseSvc.getMetadata().getName()); RecordedRequest recordedRequest = server.getLastRequest(); assertEquals("PUT", recordedRequest.getMethod()); - assertEquals("{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app\":\"httpbin\"},\"name\":\"httpbin\"},\"spec\":{\"clusterIP\":\"10.96.129.1\",\"ports\":[{\"name\":\"http\",\"port\":5511,\"targetPort\":8080}],\"selector\":{\"deploymentconfig\":\"httpbin\"}}}", + assertEquals("{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{\"foo\":\"bar\"},\"labels\":{\"app\":\"httpbin\"},\"name\":\"httpbin\"},\"spec\":{\"clusterIP\":\"10.96.129.1\",\"ports\":[{\"name\":\"http\",\"port\":5511,\"targetPort\":8080}],\"selector\":{\"deploymentconfig\":\"httpbin\"}}}", recordedRequest.getBody().readUtf8()); } diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedClusterScopeCustomResourceApiTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedClusterScopeCustomResourceApiTest.java index bddb2030f16..517133dd11e 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedClusterScopeCustomResourceApiTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedClusterScopeCustomResourceApiTest.java @@ -34,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; +import java.net.HttpURLConnection; import java.util.Collections; import static org.junit.Assert.assertEquals; @@ -91,8 +92,9 @@ void list() { @Test void createOrReplace() { - server.expect().get().withPath("/apis/example.crd.com/v1alpha1/stars/sun").andReturn(200, getStar()).times(2); - server.expect().put().withPath("/apis/example.crd.com/v1alpha1/stars/sun").andReturn(200, getStar()).once(); + server.expect().get().withPath("/apis/example.crd.com/v1alpha1/stars/sun").andReturn(HttpURLConnection.HTTP_OK, getStar()).times(2); + server.expect().post().withPath("/apis/example.crd.com/v1alpha1/stars").andReturn(HttpURLConnection.HTTP_CONFLICT, getStar()).times(2); + server.expect().put().withPath("/apis/example.crd.com/v1alpha1/stars/sun").andReturn(HttpURLConnection.HTTP_OK, getStar()).once(); starClient = server.getClient().customResources(crdContext, Star.class, StarList.class, DoneableStar.class); Star returnedStar = starClient.inNamespace("test").createOrReplace(getStar()); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedCustomResourceApiTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedCustomResourceApiTest.java index feed9effe2f..286fd15a836 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedCustomResourceApiTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/TypedCustomResourceApiTest.java @@ -30,6 +30,7 @@ import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import okhttp3.mockwebserver.RecordedRequest; +import java.net.HttpURLConnection; import java.util.Collections; import org.junit.Rule; @@ -92,8 +93,9 @@ void list() { @Test void createOrReplace() { - server.expect().get().withPath("/apis/demo.k8s.io/v1alpha1/namespaces/test/podsets/example-podset").andReturn(200, getPodSet()).times(2); - server.expect().put().withPath("/apis/demo.k8s.io/v1alpha1/namespaces/test/podsets/example-podset").andReturn(200, getPodSet()).once(); + server.expect().get().withPath("/apis/demo.k8s.io/v1alpha1/namespaces/test/podsets/example-podset").andReturn(HttpURLConnection.HTTP_OK, getPodSet()).times(2); + server.expect().post().withPath("/apis/demo.k8s.io/v1alpha1/namespaces/test/podsets").andReturn(HttpURLConnection.HTTP_CONFLICT, getPodSet()).times(2); + server.expect().put().withPath("/apis/demo.k8s.io/v1alpha1/namespaces/test/podsets/example-podset").andReturn(HttpURLConnection.HTTP_OK, getPodSet()).once(); podSetClient = server.getClient().customResources(crdContext, PodSet.class, PodSetList.class, DoneablePodSet.class); PodSet returnedPodSet = podSetClient.inNamespace("test").createOrReplace(getPodSet());