From 17104b069b680b9492ddcb1a50c36092feafdbb3 Mon Sep 17 00:00:00 2001 From: shawkins Date: Wed, 2 Jun 2021 07:16:38 -0400 Subject: [PATCH] fix #3144 #3194: making the crud mock logic more crd aware also working around making the namespace and kind available from the path --- CHANGELOG.md | 2 + .../base/CustomResourceDefinitionContext.java | 19 +++- .../CustomResourceDefinitionProcessor.java | 71 ++++++++++++++ .../mock/KubernetesAttributesExtractor.java | 94 ++++++++++++------- .../server/mock/KubernetesCrudDispatcher.java | 75 ++++++++++----- ...KubernetesCrudAttributesExtractorTest.java | 28 +++--- .../client/mock/CustomResourceCrudTest.java | 53 +++++++++++ .../kubernetes/client/mock/PodCrudTest.java | 2 +- .../src/test/resources/crontab-crd.yml | 3 + 9 files changed, 271 insertions(+), 76 deletions(-) create mode 100644 kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/CustomResourceDefinitionProcessor.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 10e15d8ecee..08dbefc5d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ * Fix #3121: ServiceOperationImpl replace will throw a KubernetesClientException rather than a NPE if the item doesn't exist * Fix #3189: VersionInfo contains null data in OpenShift 4.6 * Fix #3190: Ignore fields with name "-" when using the Go to JSON schema generator +* Fix #3144: walking back the assumption that resource/status should be a subresource for the crud mock server, now it will be only if a registered crd indicates that it should be +* Fix #3194: the mock server will now infer the namespace from the path #### Improvements * Fix #3078: adding javadocs to further clarify patch, edit, replace, etc. and note the possibility of items being modified. diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/CustomResourceDefinitionContext.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/CustomResourceDefinitionContext.java index bc727a6e057..47c4be67ee7 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/CustomResourceDefinitionContext.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/base/CustomResourceDefinitionContext.java @@ -34,6 +34,7 @@ public class CustomResourceDefinitionContext { private String plural; private String version; private String kind; + private boolean statusSubresource; public String getName() { return name; } @@ -56,6 +57,10 @@ public String getVersion() { public String getKind() { return kind; } + + public boolean isStatusSubresource() { + return statusSubresource; + } @SuppressWarnings("rawtypes") public static CustomResourceDefinitionBuilder v1beta1CRDFromCustomResourceType(Class customResource) { @@ -136,19 +141,26 @@ public static CustomResourceDefinitionContext fromCrd(CustomResourceDefinition c .withName(crd.getMetadata().getName()) .withPlural(spec.getNames().getPlural()) .withKind(spec.getNames().getKind()) + .withStatusSubresource(spec.getSubresources() != null && spec.getSubresources().getStatus() != null) .build(); } public static CustomResourceDefinitionContext fromCrd( io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition crd ) { + String version = getVersion(crd.getSpec()); return new CustomResourceDefinitionContext.Builder() .withGroup(crd.getSpec().getGroup()) - .withVersion(getVersion(crd.getSpec())) + .withVersion(version) .withScope(crd.getSpec().getScope()) .withName(crd.getMetadata().getName()) .withPlural(crd.getSpec().getNames().getPlural()) .withKind(crd.getSpec().getNames().getKind()) + .withStatusSubresource(crd.getSpec() + .getVersions() + .stream() + .filter(v -> version.equals(v.getName())) + .anyMatch(v -> v.getSubresources() != null && v.getSubresources().getStatus() != null)) .build(); } @@ -214,6 +226,11 @@ public Builder withKind(String kind) { this.customResourceDefinitionContext.kind = kind; return this; } + + public Builder withStatusSubresource(boolean statusSubresource) { + this.customResourceDefinitionContext.statusSubresource = statusSubresource; + return this; + } public CustomResourceDefinitionContext build() { return this.customResourceDefinitionContext; diff --git a/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/CustomResourceDefinitionProcessor.java b/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/CustomResourceDefinitionProcessor.java new file mode 100644 index 00000000000..61cb25a553b --- /dev/null +++ b/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/CustomResourceDefinitionProcessor.java @@ -0,0 +1,71 @@ +/** + * 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.server.mock; + +import io.fabric8.kubernetes.api.model.apiextensions.v1.CustomResourceDefinition; +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; +import io.fabric8.kubernetes.client.utils.Serialization; + +import java.util.Optional; + +/** + * Holds state related to crds by manipulating the crds known to the attributes extractor + */ +public class CustomResourceDefinitionProcessor { + + private static final String V1BETA1_PATH = "/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions"; + private static final String V1_PATH = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"; + + private KubernetesAttributesExtractor extractor; + + public CustomResourceDefinitionProcessor(KubernetesAttributesExtractor extractor) { + this.extractor = extractor; + } + + public void process(String path, String crdString, boolean delete) { + CustomResourceDefinitionContext context = null; + if (path.startsWith(V1BETA1_PATH)) { + io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition crd = Serialization + .unmarshal(crdString, io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition.class); + context = CustomResourceDefinitionContext.fromCrd(crd); + } else if (path.startsWith(V1_PATH)) { + CustomResourceDefinition crd = Serialization.unmarshal(crdString, CustomResourceDefinition.class); + context = CustomResourceDefinitionContext.fromCrd(crd); + } else { + return; + } + if (delete) { + extractor.getCrdContexts().remove(context.getPlural()); + } else { + extractor.getCrdContexts().put(context.getPlural(), context); + } + } + + public boolean isStatusSubresource(String kind) { + if (kind == null) { + return false; + } + // we are only holding by plural, lookup now by kind + Optional context = + extractor.getCrdContexts().values().stream().filter(c -> kind.equalsIgnoreCase(c.getKind())).findFirst(); + if (!context.isPresent()) { + return false; + } + return context.get().isStatusSubresource(); + } + +} diff --git a/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractor.java b/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractor.java index 79701cda6f0..a55aea44065 100644 --- a/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractor.java +++ b/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractor.java @@ -15,31 +15,33 @@ */ package io.fabric8.kubernetes.client.server.mock; -import static io.fabric8.mockwebserver.crud.AttributeType.EXISTS; -import static io.fabric8.mockwebserver.crud.AttributeType.NOT_EXISTS; -import static io.fabric8.mockwebserver.crud.AttributeType.WITHOUT; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; +import io.fabric8.kubernetes.client.utils.Serialization; +import io.fabric8.kubernetes.client.utils.Utils; +import io.fabric8.mockwebserver.crud.Attribute; +import io.fabric8.mockwebserver.crud.AttributeExtractor; +import io.fabric8.mockwebserver.crud.AttributeSet; +import okhttp3.HttpUrl; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; -import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.utils.Serialization; -import io.fabric8.kubernetes.client.utils.Utils; -import io.fabric8.mockwebserver.crud.Attribute; -import io.fabric8.mockwebserver.crud.AttributeExtractor; -import io.fabric8.mockwebserver.crud.AttributeSet; -import okhttp3.HttpUrl; +import static io.fabric8.mockwebserver.crud.AttributeType.EXISTS; +import static io.fabric8.mockwebserver.crud.AttributeType.NOT_EXISTS; +import static io.fabric8.mockwebserver.crud.AttributeType.WITHOUT; public class KubernetesAttributesExtractor implements AttributeExtractor { @@ -77,14 +79,14 @@ public class KubernetesAttributesExtractor implements AttributeExtractor crdContexts; + private Map crdContexts; public KubernetesAttributesExtractor() { - this.crdContexts = Collections.emptyList(); + this(Collections.emptyList()); } public KubernetesAttributesExtractor(List crdContexts) { - this.crdContexts = crdContexts; + this.crdContexts = crdContexts.stream().collect(Collectors.toMap(CustomResourceDefinitionContext::getPlural, Function.identity())); } private HttpUrl parseUrlFromPathAndQuery(String s) { @@ -94,6 +96,23 @@ private HttpUrl parseUrlFromPathAndQuery(String s) { return HttpUrl.parse(String.format("%s://%s%s", SCHEME, HOST, s)); } + /** + * Get the name, namespace, and kind from the path + */ + public Map fromKubernetesPath(String s) { + if (s == null || s.isEmpty()) { + return Collections.emptyMap(); + } + + //Get paths + HttpUrl url = parseUrlFromPathAndQuery(s); + Matcher m = PATTERN.matcher(url.encodedPath()); + if (m.matches()) { + return extract(m); + } + return Collections.emptyMap(); + } + @Override public AttributeSet fromPath(String s) { if (s == null || s.isEmpty()) { @@ -104,7 +123,10 @@ public AttributeSet fromPath(String s) { HttpUrl url = parseUrlFromPathAndQuery(s); Matcher m = PATTERN.matcher(url.encodedPath()); if (m.matches()) { - AttributeSet set = extract(m, crdContexts); + AttributeSet set = new AttributeSet(extract(m).entrySet() + .stream() + .map(e -> new Attribute(e.getKey(), e.getValue())) + .collect(Collectors.toList())); set = AttributeSet.merge(set, extractQueryParameters(url)); LOGGER.debug("fromPath {} : {}", s, set); return set; @@ -167,24 +189,24 @@ protected AttributeSet extractMetadataAttributes(HasMetadata hasMetadata) { return metadataAttributes; } - private static AttributeSet extract(Matcher m, List crdContexts) { - AttributeSet attributes = new AttributeSet(); + private Map extract(Matcher m) { + Map attributes = new HashMap<>(); if (m.matches()) { String kind = m.group(KIND); if (!Utils.isNullOrEmpty(kind)) { - kind = resolveKindFromPlural(crdContexts, kind); - attributes = attributes.add(new Attribute(KIND, kind)); + kind = resolveKindFromPlural(kind); + attributes.put(KIND, kind); } String namespace = m.group(NAMESPACE); if (!Utils.isNullOrEmpty(namespace)) { - attributes = attributes.add(new Attribute(NAMESPACE, namespace)); + attributes.put(NAMESPACE, namespace); } try { String name = m.group(NAME); if (!Utils.isNullOrEmpty(name)) { - attributes = attributes.add(new Attribute(NAME, name)); + attributes.put(NAME, name); } } catch (IllegalArgumentException e) { //group is missing, which is perfectly valid for create, update etc requests. @@ -193,9 +215,10 @@ private static AttributeSet extract(Matcher m, List crdContexts, String kind) { - if (isCustomResourceKind(crdContexts, kind)) { - return getCustomResourceKindFromPlural(crdContexts, kind); + private String resolveKindFromPlural(String kind) { + String result = getCustomResourceKindFromPlural(kind); + if (result != null) { + return result; } return getKindFromPluralForKubernetesTypes(kind); } @@ -285,16 +308,15 @@ private static HasMetadata toRawHasMetadata(String s) { } } - private static boolean isCustomResourceKind(List crdContexts, String kind) { - return crdContexts.stream() - .anyMatch(c -> c.getPlural().equals(kind)); + private String getCustomResourceKindFromPlural(String plural) { + CustomResourceDefinitionContext crdContext = crdContexts.get(plural); + return crdContext != null && crdContext.getKind() != null ? crdContext.getKind().toLowerCase() : null; } - private static String getCustomResourceKindFromPlural(List crdContexts, String kind) { - CustomResourceDefinitionContext crdContext = crdContexts.stream() - .filter(c -> c.getPlural().equals(kind)) - .findAny() - .orElse(null); - return crdContext != null && crdContext.getKind() != null ? crdContext.getKind().toLowerCase() : null; + /** + * A mapping of plural name to context + */ + public Map getCrdContexts() { + return crdContexts; } } diff --git a/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudDispatcher.java b/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudDispatcher.java index 2128ae01cd5..874bc723b32 100644 --- a/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudDispatcher.java +++ b/kubernetes-server-mock/src/main/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudDispatcher.java @@ -48,6 +48,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.CopyOnWriteArraySet; @@ -77,6 +78,8 @@ public class KubernetesCrudDispatcher extends CrudDispatcher { private static final Logger LOGGER = LoggerFactory.getLogger(KubernetesCrudDispatcher.class); public static final int HTTP_UNPROCESSABLE_ENTITY = 422; private final Set watchEventListeners = new CopyOnWriteArraySet<>(); + private final CustomResourceDefinitionProcessor crdProcessor; + private final KubernetesAttributesExtractor kubernetesAttributesExtractor; public KubernetesCrudDispatcher() { this(Collections.emptyList()); @@ -88,6 +91,8 @@ public KubernetesCrudDispatcher(List crdContext public KubernetesCrudDispatcher(KubernetesCrudAttributesExtractor attributeExtractor, ResponseComposer responseComposer) { super(new Context(Serialization.jsonMapper()), attributeExtractor, responseComposer); + this.kubernetesAttributesExtractor = attributeExtractor; + crdProcessor = new CustomResourceDefinitionProcessor(kubernetesAttributesExtractor); } @Override @@ -183,7 +188,11 @@ public MockResponse handlePatch(String path, String s, String contentType) { JsonNode source = context.getMapper().readTree(body); JsonNode status = null; - if (!isStatusPath(path)) { + Map pathValues = kubernetesAttributesExtractor.fromKubernetesPath(path); + boolean statusSubresource = + crdProcessor.isStatusSubresource(pathValues.get(KubernetesAttributesExtractor.KIND)); + + if (statusSubresource && !isStatusPath(path)) { status = removeStatus(source); } @@ -202,10 +211,12 @@ public MockResponse handlePatch(String path, String s, String contentType) { } // restore the status - if (status == null) { - removeStatus(updated); - } else { - ((ObjectNode)updated).set(STATUS, status); + if (statusSubresource || isStatusPath(path)) { + if (status == null) { + removeStatus(updated); + } else { + ((ObjectNode)updated).set(STATUS, status); + } } String updatedAsString = context.getMapper().writeValueAsString(updated); @@ -218,9 +229,15 @@ public MockResponse handlePatch(String path, String s, String contentType) { .filter(entry -> entry.getKey().matches(query)) .findFirst().orElseThrow(IllegalStateException::new).getKey(); + if (body.equals(updatedAsString)) { + response.setResponseCode(HttpURLConnection.HTTP_ACCEPTED); + response.setBody(updatedAsString); + return response; + } map.remove(attributeSet); AttributeSet newAttributeSet = AttributeSet.merge(attributeSet, attributeExtractor.fromResource(updatedAsString)); map.put(newAttributeSet, updatedAsString); + crdProcessor.process(path, updatedAsString, false); final AtomicBoolean flag = new AtomicBoolean(false); AttributeSet finalAttributeSet = attributeSet; @@ -348,7 +365,8 @@ private int doDelete(String path, String event) { .filter(listener -> listener.attributeMatches(item)) .forEach(listener -> listener.sendWebSocketResponse(map.get(item), event)); } - map.remove(item); + String existing = map.remove(item); + crdProcessor.process(path, existing, true); }); return HttpURLConnection.HTTP_OK; } @@ -361,44 +379,56 @@ private List findItems(AttributeSet query) { private MockResponse doCreateOrModify(String path, String initial, String event) { MockResponse mockResponse = new MockResponse(); + // workaround for mockserver https://github.com/fabric8io/mockwebserver/pull/59 + Map pathValues = kubernetesAttributesExtractor.fromKubernetesPath(path); AttributeSet attributes = attributeExtractor.fromPath(path); try { JsonNode source = context.getMapper().readTree(initial); - JsonNode status = removeStatus(source); int responseCode = HttpURLConnection.HTTP_OK; if (ADDED.equals(event)) { - HasMetadata h = toKubernetesResource(context.getMapper().writeValueAsString(source)); + HasMetadata h = toKubernetesResource(initial); if (h != null && h.getMetadata() != null && h.getMetadata().getName() != null) { - attributes = AttributeSet.merge(attributes, new AttributeSet(new Attribute("name", h.getMetadata().getName()))); + attributes = AttributeSet.merge(attributes, new AttributeSet(new Attribute(KubernetesAttributesExtractor.NAME, h.getMetadata().getName()))); } } + boolean statusSubresource = crdProcessor.isStatusSubresource(pathValues.get(KubernetesAttributesExtractor.KIND)); + List items = findItems(attributes); if (items.isEmpty()) { if (MODIFIED.equals(event)) { responseCode = HttpURLConnection.HTTP_NOT_FOUND; } else { - setDefaultMetadata(source, attributes, null); + if (statusSubresource) { + removeStatus(source); + } + setDefaultMetadata(source, pathValues, null); } } else if (ADDED.equals(event)) { responseCode = HttpURLConnection.HTTP_CONFLICT; } else if (MODIFIED.equals(event)) { String existing = map.remove(items.get(0)); JsonNode existingNode = context.getMapper().readTree(existing); + JsonNode status = null; if (isStatusPath(path)) { + status = removeStatus(source); // set the status on the existing node source = existingNode; } else { - // set the status on the new node and preserve generated fields - status = removeStatus(existingNode); - setDefaultMetadata(source, attributes, existingNode.findValue("metadata")); + // preserve status and generated fields + if (statusSubresource) { + status = removeStatus(existingNode); + } + setDefaultMetadata(source, pathValues, existingNode.findValue("metadata")); } - if (status != null) { - ((ObjectNode) source).set(STATUS, status); - } else { - ((ObjectNode) source).remove(STATUS); + if (statusSubresource || isStatusPath(path)) { + if (status != null) { + ((ObjectNode) source).set(STATUS, status); + } else { + ((ObjectNode) source).remove(STATUS); + } } // re-read without modifications existingNode = context.getMapper().readTree(existing); @@ -410,8 +440,9 @@ private MockResponse doCreateOrModify(String path, String initial, String event) if (responseCode == HttpURLConnection.HTTP_OK) { String s = context.getMapper().writeValueAsString(source); AttributeSet features = AttributeSet.merge(attributes, attributeExtractor.fromResource(s)); - map.put(features, s); + map.put(features, s); // always add back as it was proactively removed if (event != null && !event.isEmpty()) { + crdProcessor.process(path, initial, false); final String response = s; final String finalEvent = event; watchEventListeners.stream() @@ -431,17 +462,15 @@ private static boolean isStatusPath(String path) { return path.endsWith("/" + STATUS); } - private void setDefaultMetadata(JsonNode source, AttributeSet fromPath, JsonNode exitingMetadata) throws JsonProcessingException { + private void setDefaultMetadata(JsonNode source, Map pathValues, JsonNode exitingMetadata) { ObjectNode metadata = (ObjectNode)source.findValue("metadata"); UUID uuid = UUID.randomUUID(); if (metadata.get("name") == null) { metadata.put("name", metadata.get("generateName").asText() + "-" + uuid.toString()); } - // needs a later release of mockwebserver - /* if (metadata.get("namespace") == null) { - metadata.put("namespace", fromPath.getAttribute("namespace").getValue().toString()); - }*/ + metadata.put("namespace", pathValues.get(KubernetesAttributesExtractor.NAMESPACE)); + } metadata.put("uid", getOrDefault(exitingMetadata, "uid", uuid.toString())); // resourceVersion is not yet handled appropriately metadata.put("resourceVersion", "1"); diff --git a/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudAttributesExtractorTest.java b/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudAttributesExtractorTest.java index 8c7083d68cb..f0a40bfca22 100644 --- a/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudAttributesExtractorTest.java +++ b/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesCrudAttributesExtractorTest.java @@ -263,6 +263,7 @@ public void shouldGenerateMetadata() { .getItems().get(0); assertTrue(result.getMetadata().getName().startsWith("prefix")); + assertNotNull(result.getMetadata().getNamespace()); assertNotNull(result.getMetadata().getUid()); assertNotNull(result.getMetadata().getResourceVersion()); assertNotNull(result.getMetadata().getCreationTimestamp()); @@ -276,7 +277,6 @@ public void replaceNonExistent() { KubernetesClient kubernetesClient = kubernetesServer.getClient(); Pod pod = new PodBuilder().withNewMetadata() .withName("name") - .withNamespace("test") // required until https://github.com/fabric8io/mockwebserver/pull/59 .endMetadata() .withStatus(new PodStatusBuilder().withHostIP("x").build()) .build(); @@ -287,20 +287,19 @@ public void replaceNonExistent() { } @Test - public void statusHandling() { + void nonSubresourceStatusHandling() { KubernetesServer kubernetesServer = new KubernetesServer(false, true); kubernetesServer.before(); KubernetesClient kubernetesClient = kubernetesServer.getClient(); Pod pod = new PodBuilder().withNewMetadata() .withName("name") - .withNamespace("test") // required until https://github.com/fabric8io/mockwebserver/pull/59 .endMetadata() .withStatus(new PodStatusBuilder().withHostIP("x").build()) .build(); Pod result = kubernetesClient.pods().create(pod); - // should be null after create - assertNull(result.getStatus()); + // should be non-null after create because it's not a crd marked as a status subresource + assertNotNull(result.getStatus()); Map labels = new HashMap<>(); labels.put("app", "core"); @@ -314,28 +313,31 @@ public void statusHandling() { String originalUid = result.getMetadata().getUid(); - // should be null after replace - assertNull(result.getStatus()); + // should be non-null after replace + assertNotNull(result.getStatus()); + // should be a no-op assertNotNull(kubernetesClient.pods().updateStatus(pod).getStatus()); labels.put("other", "label"); + pod.getStatus().setHostIP("y"); result = kubernetesClient.pods() .inNamespace(pod.getMetadata().getNamespace()) .withName(pod.getMetadata().getName()) .replace(pod); - // should retain the existing - assertNotNull(result.getStatus()); + // should replace the existing + assertEquals("y", result.getStatus().getHostIP()); labels.put("another", "label"); + pod.getStatus().setHostIP("z"); result = kubernetesClient.pods() .inNamespace(pod.getMetadata().getNamespace()) .withName(pod.getMetadata().getName()) .patch(pod); - // should retain the existing - assertNotNull(result.getStatus()); + // should replace the existing + assertEquals("z", result.getStatus().getHostIP()); assertEquals(originalUid, result.getMetadata().getUid()); } @@ -347,7 +349,6 @@ public void jsonPatchStatus() { KubernetesClient kubernetesClient = kubernetesServer.getClient(); Pod pod = new PodBuilder().withNewMetadata() .withName("name") - .withNamespace("test") // required until https://github.com/fabric8io/mockwebserver/pull/59 .endMetadata() .build(); Pod result = kubernetesClient.pods().create(pod); @@ -380,7 +381,6 @@ public void patchStatus() { KubernetesClient kubernetesClient = kubernetesServer.getClient(); Pod pod = new PodBuilder().withNewMetadata() .withName("name") - .withNamespace("test") // required until https://github.com/fabric8io/mockwebserver/pull/59 .endMetadata() .build(); Pod result = kubernetesClient.pods().create(pod); @@ -409,7 +409,6 @@ public void createConflict() { KubernetesClient kubernetesClient = kubernetesServer.getClient(); Pod pod = new PodBuilder().withNewMetadata() .withName("name") - .withNamespace("test") // required until https://github.com/fabric8io/mockwebserver/pull/59 .endMetadata() .withStatus(new PodStatusBuilder().withHostIP("x").build()) .build(); @@ -426,7 +425,6 @@ public void testDuplicateUpdateEvent() throws Exception { KubernetesClient kubernetesClient = kubernetesServer.getClient(); Pod pod = new PodBuilder().withNewMetadata() .withName("name") - .withNamespace("test") // required until https://github.com/fabric8io/mockwebserver/pull/59 .endMetadata() .withStatus(new PodStatusBuilder().withHostIP("x").build()) .build(); 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 5abcd667db0..678059360fc 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 @@ -20,23 +20,28 @@ import io.fabric8.kubernetes.api.model.apiextensions.v1beta1.CustomResourceDefinition; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; +import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; import io.fabric8.kubernetes.client.dsl.internal.RawCustomResourceOperationsImpl; import io.fabric8.kubernetes.client.mock.crd.CronTab; import io.fabric8.kubernetes.client.mock.crd.CronTabSpec; +import io.fabric8.kubernetes.client.mock.crd.CronTabStatus; import io.fabric8.kubernetes.client.server.mock.EnableKubernetesMockClient; import io.fabric8.kubernetes.internal.KubernetesDeserializer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @EnableKubernetesMockClient(crud = true) @@ -147,6 +152,54 @@ void testCrudWithDashSymbolInCRDName() { assertEquals(2, cronTabList.getItems().size()); } + @Test + void testStatusSubresourceHandling() { + CronTab cronTab = createCronTab("my-new-cron-object", "* * * * */5", 3, "my-awesome-cron-image"); + CronTabStatus status = new CronTabStatus(); + status.setReplicas(1); + cronTab.setStatus(status); + + NonNamespaceOperation, Resource> cronTabClient = client + .customResources(CronTab.class).inNamespace("test-ns"); + + CronTab result = cronTabClient.create(cronTab); + + // should be null after create + assertNull(result.getStatus()); + + Map labels = new HashMap<>(); + labels.put("app", "core"); + + cronTab.getMetadata().setLabels(labels); + + result = cronTabClient.replace(cronTab); + + String originalUid = result.getMetadata().getUid(); + + // should be null after replace + assertNull(result.getStatus()); + + assertNotNull(cronTabClient.updateStatus(cronTab).getStatus()); + + labels.put("other", "label"); + cronTab.setStatus(null); + + result = cronTabClient.replace(cronTab); + + // should retain the existing + assertNotNull(result.getStatus()); + + labels.put("another", "label"); + result = cronTabClient.patch(cronTab); + + // should retain the existing + assertNotNull(result.getStatus()); + // should have accumulated all labels + assertEquals(new HashSet(Arrays.asList("app", "other", "another")), result.getMetadata().getLabels().keySet()); + + assertEquals(originalUid, result.getMetadata().getUid()); + } + void assertCronTab(CronTab cronTab, String name, String cronTabSpec, int replicas, String image) { assertEquals(name, cronTab.getMetadata().getName()); assertEquals(cronTabSpec, cronTab.getSpec().getCronSpec()); diff --git a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodCrudTest.java b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodCrudTest.java index f505f1480fc..ffe1c322802 100644 --- a/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodCrudTest.java +++ b/kubernetes-tests/src/test/java/io/fabric8/kubernetes/client/mock/PodCrudTest.java @@ -105,7 +105,7 @@ void testPodWatchOnName() throws InterruptedException { Watch watch = client.pods().inNamespace("ns1").withName(pod1.getMetadata().getName()).watch(lw); pod1 = client.pods().inNamespace("ns1").withName(pod1.getMetadata().getName()) - .patch(new PodBuilder().withNewMetadataLike(pod1.getMetadata()).endMetadata().build()); + .edit(p->new PodBuilder(p).editMetadata().withLabels(Collections.singletonMap("x", "y")).endMetadata().build()); pod1.setSpec(new PodSpecBuilder().addNewContainer().withImage("nginx").withName("nginx").endContainer().build()); diff --git a/kubernetes-tests/src/test/resources/crontab-crd.yml b/kubernetes-tests/src/test/resources/crontab-crd.yml index 448a4e17cf0..74ec9db60a8 100644 --- a/kubernetes-tests/src/test/resources/crontab-crd.yml +++ b/kubernetes-tests/src/test/resources/crontab-crd.yml @@ -64,3 +64,6 @@ spec: kind: CronTab shortNames: - ct + subresources: + status: {} +