diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed9a424451..8e1c51ec28c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### 4.10-SNAPSHOT #### Bugs +* Fix #2285: Raw CustomResource API createOrReplace does not propagate exceptions from create * Fix Raw CustomResource API path generation to not having trailing slash * Fix #2131: Failing to parse CustomResourceDefinition with OpenAPIV3Schema using JSONSchemaPropOr\* fields * Fix KubernetesAttributesExctractor to extract metadata from unregistered custom resources, such when using Raw CustomResource API diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImpl.java index 8e119ef702f..4a636aeac4e 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImpl.java @@ -15,7 +15,17 @@ */ package io.fabric8.kubernetes.client.dsl.internal; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + import com.fasterxml.jackson.databind.ObjectMapper; + import io.fabric8.kubernetes.api.model.DeleteOptions; import io.fabric8.kubernetes.api.model.DeletionPropagation; import io.fabric8.kubernetes.api.model.ListOptions; @@ -38,14 +48,6 @@ import okhttp3.RequestBody; import okhttp3.Response; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - /** * This class simple does basic operations for custom defined resources without * demanding the POJOs for custom resources. It is serializing/deserializing @@ -55,6 +57,9 @@ * */ public class RawCustomResourceOperationsImpl extends OperationSupport { + + private static final String METADATA = "metadata"; + private static final String RESOURCE_VERSION = "resourceVersion"; private OkHttpClient client; private Config config; private CustomResourceDefinitionContext customResourceDefinition; @@ -174,7 +179,7 @@ public Map create(String namespace, Map object) * @throws IOException in case of network/serializiation failures or failures from Kuberntes API */ public Map createOrReplace(String objectAsString) throws IOException { - return createOrReplaceJsonStringObject(null, objectAsString); + return createOrReplaceObject(null, load(objectAsString)); } /** @@ -185,7 +190,7 @@ public Map createOrReplace(String objectAsString) throws IOExcep * @throws IOException in case of network/serialization failures or failures from Kubernetes API */ public Map createOrReplace(Map customResourceObject) throws IOException { - return createOrReplace(objectMapper.writeValueAsString(customResourceObject)); + return createOrReplaceObject(null, customResourceObject); } /** @@ -196,7 +201,7 @@ public Map createOrReplace(Map customResourceObj * @throws IOException in case of network/serialization failures or failures from Kubernetes API */ public Map createOrReplace(InputStream inputStream) throws IOException { - return createOrReplace(IOHelpers.readFully(inputStream)); + return createOrReplaceObject(null, load(inputStream)); } /** @@ -208,7 +213,7 @@ public Map createOrReplace(InputStream inputStream) throws IOExc * @throws IOException in case of network/serialization failures or failures from Kubernetes API */ public Map createOrReplace(String namespace, String objectAsString) throws IOException { - return createOrReplaceJsonStringObject(namespace, objectAsString); + return createOrReplaceObject(namespace, load(objectAsString)); } /** @@ -220,19 +225,19 @@ public Map createOrReplace(String namespace, String objectAsStri * @throws IOException in case of network/serialization failures or failures from Kubernetes API */ public Map createOrReplace(String namespace, Map customResourceObject) throws IOException { - return createOrReplace(namespace, objectMapper.writeValueAsString(customResourceObject)); + return createOrReplaceObject(namespace, customResourceObject); } /** * Create or replace a custom resource which is namespaced object. * * @param namespace desired namespace - * @param objectAsString object as file input stream + * @param objectAsStream object as file input stream * @return Object as HashMap * @throws IOException in case of network/serialization failures or failures from Kubernetes API */ - public Map createOrReplace(String namespace, InputStream objectAsString) throws IOException { - return createOrReplace(namespace, IOHelpers.readFully(objectAsString)); + public Map createOrReplace(String namespace, InputStream objectAsStream) throws IOException { + return createOrReplaceObject(namespace, load(objectAsStream)); } /** @@ -680,22 +685,39 @@ public Watch watch(String namespace, String name, Map labels, Li } - private Map createOrReplaceJsonStringObject(String namespace, String objectAsString) throws IOException { + private Map createOrReplaceObject(String namespace, Map objectAsMap) throws IOException { + Map metadata = (Map) objectAsMap.get(METADATA); + if (metadata == null) { + throw KubernetesClientException.launderThrowable(new IllegalStateException("Invalid object provided -- metadata is required.")); + } + Map ret; + + // can't include resourceVersion in create calls + String originalResourceVersion = (String) metadata.get(RESOURCE_VERSION); + metadata.remove(RESOURCE_VERSION); + try { if(namespace != null) { - ret = create(namespace, objectAsString); + ret = create(namespace, objectAsMap); } else { - ret = create(objectAsString); + ret = create(objectAsMap); } } catch (KubernetesClientException exception) { + if (exception.getCode() != HttpURLConnection.HTTP_CONFLICT) { + throw exception; + } + try { - Map objectMap = load(objectAsString); - String name = ((Map) objectMap.get("metadata")).get("name").toString(); - ret = namespace != null ? - edit(namespace, name, objectAsString) : edit(name, objectAsString); + // re-add for edit call + if (originalResourceVersion != null) { + metadata.put(RESOURCE_VERSION, originalResourceVersion); + } + String name = (String) metadata.get("name"); + ret = namespace != null ? + edit(namespace, name, objectAsMap) : edit(name, objectAsMap); } catch (NullPointerException nullPointerException) { - throw KubernetesClientException.launderThrowable(new IllegalStateException("Invalid json string provided.")); + throw KubernetesClientException.launderThrowable(new IllegalStateException("Invalid object provided -- metadata.name is required.")); } } return ret; @@ -832,10 +854,10 @@ private Request getRequest(String url, String body, HttpCallMethod httpCallMetho private String appendResourceVersionInObject(String namespace, String customResourceName, String customResourceAsJsonString) throws IOException { Map oldObject = get(namespace, customResourceName); - String resourceVersion = ((Map)oldObject.get("metadata")).get("resourceVersion").toString(); + String resourceVersion = ((Map)oldObject.get(METADATA)).get(RESOURCE_VERSION).toString(); Map newObject = convertJsonOrYamlStringToMap(customResourceAsJsonString); - ((Map)newObject.get("metadata")).put("resourceVersion", resourceVersion); + ((Map)newObject.get(METADATA)).put(RESOURCE_VERSION, resourceVersion); return objectMapper.writeValueAsString(newObject); } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImplTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImplTest.java index 34c8f27ab0b..7e6cc3eba6f 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImplTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/dsl/internal/RawCustomResourceOperationsImplTest.java @@ -15,37 +15,41 @@ */ package io.fabric8.kubernetes.client.dsl.internal; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + import io.fabric8.kubernetes.api.model.ListOptionsBuilder; import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; import io.fabric8.kubernetes.client.utils.Utils; import okhttp3.Call; import okhttp3.HttpUrl; import okhttp3.MediaType; import okhttp3.OkHttpClient; +import okhttp3.Protocol; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import org.bouncycastle.cert.ocsp.Req; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.*; - public class RawCustomResourceOperationsImplTest { private OkHttpClient mockClient; private Config config; private CustomResourceDefinitionContext customResourceDefinitionContext; + private Response mockSuccessResponse; @BeforeEach public void setUp() throws IOException { @@ -60,11 +64,9 @@ public void setUp() throws IOException { .build(); Call mockCall = mock(Call.class); - Response mockResponse = mock(Response.class); - when(mockResponse.isSuccessful()).thenReturn(true); - when(mockResponse.body()).thenReturn(ResponseBody.create(MediaType.get("application/json"), "")); + mockSuccessResponse = mockResponse(HttpURLConnection.HTTP_OK); when(mockCall.execute()) - .thenReturn(mockResponse); + .thenReturn(mockSuccessResponse); when(mockClient.newCall(any())).thenReturn(mockCall); } @@ -75,15 +77,44 @@ void testCreateOrReplaceUrl() throws IOException { String resourceAsString = "{\"metadata\":{\"name\":\"myresource\",\"namespace\":\"myns\"}, \"kind\":\"raw\", \"apiVersion\":\"v1\"}"; ArgumentCaptor captor = ArgumentCaptor.forClass(Request.class); + Call mockCall = mock(Call.class); + Response mockErrorResponse = mockResponse(HttpURLConnection.HTTP_INTERNAL_ERROR); + Response mockConflictResponse = mockResponse(HttpURLConnection.HTTP_CONFLICT); + when(mockCall.execute()) + .thenReturn(mockErrorResponse, mockConflictResponse, mockSuccessResponse); + when(mockClient.newCall(any())).thenReturn(mockCall); + // When + try { + rawCustomResourceOperations.createOrReplace(resourceAsString); + fail("expected first call to createOrReplace to throw exception due to 500 response"); + } catch (KubernetesClientException e) { + assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, e.getCode()); + } rawCustomResourceOperations.createOrReplace(resourceAsString); rawCustomResourceOperations.createOrReplace("myns", resourceAsString); // Then - verify(mockClient, times(2)).newCall(captor.capture()); - assertEquals(2, captor.getAllValues().size()); + verify(mockClient, times(4)).newCall(captor.capture()); + assertEquals(4, captor.getAllValues().size()); assertEquals("/apis/test.fabric8.io/v1alpha1/hellos", captor.getAllValues().get(0).url().encodedPath()); - assertEquals("/apis/test.fabric8.io/v1alpha1/namespaces/myns/hellos", captor.getAllValues().get(1).url().encodedPath()); + assertEquals("POST", captor.getAllValues().get(0).method()); + assertEquals("/apis/test.fabric8.io/v1alpha1/hellos", captor.getAllValues().get(1).url().encodedPath()); + assertEquals("POST", captor.getAllValues().get(1).method()); + assertEquals("/apis/test.fabric8.io/v1alpha1/hellos/myresource", captor.getAllValues().get(2).url().encodedPath()); + assertEquals("PUT", captor.getAllValues().get(2).method()); + assertEquals("/apis/test.fabric8.io/v1alpha1/namespaces/myns/hellos", captor.getAllValues().get(3).url().encodedPath()); + assertEquals("POST", captor.getAllValues().get(3).method()); + } + + private Response mockResponse(int code) { + return new Response.Builder() + .request(new Request.Builder().url("http://mock").build()) + .protocol(Protocol.HTTP_1_1) + .code(code) + .body(ResponseBody.create(MediaType.get("application/json"), "")) + .message("mock") + .build(); } @Test diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/RawCustomResourceIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/RawCustomResourceIT.java index b914c1233a0..c2a707105da 100644 --- a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/RawCustomResourceIT.java +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/RawCustomResourceIT.java @@ -15,10 +15,14 @@ */ package io.fabric8.kubernetes; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; -import io.fabric8.kubernetes.client.KubernetesClient; -import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + import org.arquillian.cube.kubernetes.api.Session; import org.arquillian.cube.kubernetes.impl.requirement.RequiresKubernetes; import org.arquillian.cube.requirement.ArquillianConditionalRunner; @@ -28,13 +32,11 @@ import org.junit.Test; import org.junit.runner.RunWith; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; -import static org.assertj.core.api.Assertions.assertThat; +import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; @RunWith(ArquillianConditionalRunner.class) @RequiresKubernetes @@ -85,12 +87,20 @@ public void testCrud() throws IOException { // Test Create via file Map object = client.customResource(customResourceDefinitionContext).create(currentNamespace, getClass().getResourceAsStream("/test-rawcustomresource.yml")); assertThat(((HashMap)object.get("metadata")).get("name")).isEqualTo("otter"); + // Test Create via raw json string String rawJsonCustomResourceObj = "{\"apiVersion\":\"jungle.example.com/v1\"," + "\"kind\":\"Animal\",\"metadata\": {\"name\": \"walrus\"}," + "\"spec\": {\"image\": \"my-awesome-walrus-image\"}}"; - object = client.customResource(customResourceDefinitionContext).create(currentNamespace, rawJsonCustomResourceObj); + object = client.customResource(customResourceDefinitionContext).createOrReplace(currentNamespace, rawJsonCustomResourceObj); + assertThat(((HashMap)object.get("metadata")).get("name")).isEqualTo("walrus"); + assertThat(((HashMap)object.get("spec")).get("image")).isEqualTo("my-awesome-walrus-image"); + + // Test replace with object + ((HashMap)object.get("spec")).put("image", "new-walrus-image"); + object = client.customResource(customResourceDefinitionContext).createOrReplace(currentNamespace, object); assertThat(((HashMap)object.get("metadata")).get("name")).isEqualTo("walrus"); + assertThat(((HashMap)object.get("spec")).get("image")).isEqualTo("new-walrus-image"); // Test Get: object = client.customResource(customResourceDefinitionContext).get(currentNamespace, "otter"); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceCrudTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceCrudTest.java index e0e7c14af35..70c8cf15f64 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceCrudTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceCrudTest.java @@ -15,37 +15,51 @@ */ package io.fabric8.kubernetes.client.mock; -import com.fasterxml.jackson.databind.ObjectMapper; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Rule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; + import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinition; -import io.fabric8.kubernetes.api.model.apiextensions.CustomResourceDefinitionBuilder; -import io.fabric8.kubernetes.api.model.apiextensions.JSONSchemaProps; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; -import io.fabric8.kubernetes.client.mock.crd.CronTabSpec; -import io.fabric8.kubernetes.client.mock.crd.DoneableCronTab; +import io.fabric8.kubernetes.client.dsl.internal.RawCustomResourceOperationsImpl; import io.fabric8.kubernetes.client.mock.crd.CronTab; import io.fabric8.kubernetes.client.mock.crd.CronTabList; +import io.fabric8.kubernetes.client.mock.crd.CronTabSpec; +import io.fabric8.kubernetes.client.mock.crd.DoneableCronTab; import io.fabric8.kubernetes.client.server.mock.KubernetesServer; import io.fabric8.kubernetes.internal.KubernetesDeserializer; -import org.junit.Rule; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.migrationsupport.rules.EnableRuleMigrationSupport; - -import java.io.IOException; -import java.net.URL; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; @EnableRuleMigrationSupport public class CustomResourceCrudTest { @Rule public KubernetesServer kubernetesServer = new KubernetesServer(true,true); + + private CustomResourceDefinition cronTabCrd; + private CustomResourceDefinitionContext crdContext; - private MixedOperation> podSetClient; + @BeforeEach + void setUp() { + KubernetesDeserializer.registerCustomKind("stable.example.com/v1", "CronTab", CronTab.class); + cronTabCrd = kubernetesServer.getClient() + .customResourceDefinitions() + .load(getClass().getResourceAsStream("/crontab-crd.yml")) + .get(); + kubernetesServer.getClient().customResourceDefinitions().create(cronTabCrd); + crdContext = CustomResourceDefinitionContext.fromCrd(cronTabCrd); + } @Test public void testCrud() throws IOException { @@ -54,11 +68,6 @@ public void testCrud() throws IOException { CronTab cronTab3 = createCronTab("my-third-cron-object", "* * * * */3", 1, "my-third-cron-image"); KubernetesClient client = kubernetesServer.getClient(); - KubernetesDeserializer.registerCustomKind("stable.example.com/v1", "CronTab", CronTab.class); - CustomResourceDefinition cronTabCrd = client.customResourceDefinitions().load(getClass().getResourceAsStream("/crontab-crd.yml")).get(); - client.customResourceDefinitions().create(cronTabCrd); - - CustomResourceDefinitionContext crdContext = CustomResourceDefinitionContext.fromCrd(cronTabCrd); MixedOperation> cronTabClient = client .customResources(crdContext, CronTab.class, CronTabList.class, DoneableCronTab.class); @@ -87,6 +96,27 @@ public void testCrud() throws IOException { assertEquals(2, cronTabList.getItems().size()); } + @Test + void testCreateOrReplaceRaw() throws IOException { + RawCustomResourceOperationsImpl raw = kubernetesServer.getClient().customResource(CustomResourceDefinitionContext.fromCrd(cronTabCrd)); + + Map object = new HashMap<>(); + Map metadata = new HashMap<>(); + metadata.put("name", "foo"); + + object.put("metadata", metadata); + object.put("spec", "initial"); + + Map created = raw.createOrReplace(object); + + assertEquals(object, created); + + object.put("spec", "updated"); + + Map updated = raw.createOrReplace(object); + assertNotEquals(created, updated); + assertEquals(object, updated); + } @Test public void testCrudWithDashSymbolInCRDName() throws IOException { @@ -95,10 +125,6 @@ public void testCrudWithDashSymbolInCRDName() throws IOException { CronTab cronTab3 = createCronTab("my-third-cron-object", "* * * * */3", 1, "my-third-cron-image"); KubernetesClient client = kubernetesServer.getClient(); - KubernetesDeserializer.registerCustomKind("stable.example.com/v1", "CronTab", CronTab.class); - CustomResourceDefinition cronTabCrd = cronTabCRDWithDashSymbolName(); - client.customResourceDefinitions().create(cronTabCrd); - MixedOperation> cronTabClient = client .customResources(cronTabCrd, CronTab.class, CronTabList.class, DoneableCronTab.class); @@ -144,35 +170,4 @@ public CronTab createCronTab(String name, String cronTabSpecStr, int replicas, S cronTab.setSpec(cronTabSpec); return cronTab; } - - public CustomResourceDefinition cronTabCRDWithDashSymbolName() throws IOException { - return new CustomResourceDefinitionBuilder() - .withApiVersion("apiextensions.k8s.io/v1beta1") - .withNewMetadata().withName("crontabs.stable.example.com") - .endMetadata() - .withNewSpec() - .withNewNames() - .withKind("CronTab") - .withPlural("cron-tabs") - .withSingular("cron-tab") - .endNames() - .withGroup("stable.example.com") - .withVersion("v1") - .withScope("Namespaced") - .withNewValidation() - .withNewOpenAPIV3SchemaLike(readSchema()) - .endOpenAPIV3Schema() - .endValidation() - .endSpec() - .build(); - - } - - private JSONSchemaProps readSchema() throws IOException { - ObjectMapper mapper = new ObjectMapper(); - final URL resource = getClass().getResource("/test-crd-validation-schema.json"); - - final JSONSchemaProps jsonSchemaProps = mapper.readValue(resource, JSONSchemaProps.class); - return jsonSchemaProps; - } } diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceTest.java index 42403d93c5e..c81e928d6a1 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/CustomResourceTest.java @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.IOException; import java.net.HttpURLConnection; @@ -32,6 +33,7 @@ import io.fabric8.kubernetes.api.model.DeleteOptions; import io.fabric8.kubernetes.api.model.ListOptions; import io.fabric8.kubernetes.api.model.ListOptionsBuilder; +import io.fabric8.kubernetes.api.model.StatusBuilder; import io.fabric8.kubernetes.client.Watch; import io.fabric8.kubernetes.client.Watcher; import io.fabric8.kubernetes.api.model.WatchEvent; @@ -91,11 +93,19 @@ public void testCreateOrReplace() throws IOException { String jsonObject = "{\"apiVersion\": \"test.fabric8.io/v1alpha1\",\"kind\": \"Hello\"," + "\"metadata\": {\"resourceVersion\":\"1\", \"name\": \"example-hello\"},\"spec\": {\"size\": 3}}"; + server.expect().post().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos").andReturn(HttpURLConnection.HTTP_INTERNAL_ERROR, new StatusBuilder().build()).once(); server.expect().post().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos").andReturn(HttpURLConnection.HTTP_CREATED, jsonObject).once(); - server.expect().get().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, jsonObject).once(); + server.expect().post().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos").andReturn(HttpURLConnection.HTTP_CONFLICT, jsonObject).once(); server.expect().put().withPath("/apis/test.fabric8.io/v1alpha1/namespaces/ns1/hellos/example-hello").andReturn(HttpURLConnection.HTTP_OK, jsonObject).once(); KubernetesClient client = server.getClient(); + try { + client.customResource(customResourceDefinitionContext).createOrReplace("ns1", jsonObject); + fail("expected to fail once due to 500"); + } catch (KubernetesClientException e) { + assertEquals(HttpURLConnection.HTTP_INTERNAL_ERROR, e.getCode()); + } + Map resource = client.customResource(customResourceDefinitionContext).createOrReplace("ns1", jsonObject); assertEquals("example-hello", ((Map)resource.get("metadata")).get("name").toString());