diff --git a/CHANGELOG.md b/CHANGELOG.md index 38b5e5f1ab8..b027fceb779 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ * 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 * Fix #3076: the MetadataObject for CustomResource is now seen as Buildable +* Fix #3216: made the mock server aware of apiVersions #### 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/utils/Serialization.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java index 812a045d54a..e7eaf0957d2 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/Serialization.java @@ -144,6 +144,20 @@ public static T unmarshal(InputStream is, ObjectMapper mapper, Map template argument denoting type + * @return returns de-serialized object + */ + public static T unmarshal(String str) { + try (InputStream is = new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8))) { + return unmarshal(is); + } catch (IOException e) { + throw KubernetesClientException.launderThrowable(e); + } + } /** * Unmarshals a {@link String} 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 index 61cb25a553b..af7040f136a 100644 --- 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 @@ -20,7 +20,7 @@ import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; import io.fabric8.kubernetes.client.utils.Serialization; -import java.util.Optional; +import java.util.Map; /** * Holds state related to crds by manipulating the crds known to the attributes extractor @@ -49,23 +49,19 @@ public void process(String path, String crdString, boolean delete) { return; } if (delete) { - extractor.getCrdContexts().remove(context.getPlural()); + extractor.removeCrdContext(context); } else { - extractor.getCrdContexts().put(context.getPlural(), context); + extractor.addCrdContext(context); } } - public boolean isStatusSubresource(String kind) { - if (kind == null) { + public boolean isStatusSubresource(Map pathValues) { + CustomResourceDefinitionContext context = extractor.getCrdContext(pathValues.get(KubernetesAttributesExtractor.API), + pathValues.get(KubernetesAttributesExtractor.VERSION), pathValues.get(KubernetesAttributesExtractor.PLURAL)); + if (context == 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(); + return context.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 c8ea9239c94..40ef0f5793d 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 @@ -17,8 +17,8 @@ import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; +import io.fabric8.kubernetes.client.utils.ApiVersionUtil; import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Utils; import io.fabric8.mockwebserver.crud.Attribute; @@ -28,16 +28,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.TreeMap; +import java.util.Objects; +import java.util.Optional; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,9 +50,14 @@ public class KubernetesAttributesExtractor implements AttributeExtractor crdContexts; - private Map pluralToKind = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private Map, CustomResourceDefinitionContext> crdContexts; public KubernetesAttributesExtractor() { this(Collections.emptyList()); } public KubernetesAttributesExtractor(List crdContexts) { - this.crdContexts = crdContexts.stream().collect(Collectors.toMap(CustomResourceDefinitionContext::getPlural, Function.identity())); + this.crdContexts = crdContexts.stream().collect(Collectors.toMap(c -> pluralKey(c), Function.identity())); + } + + private static List pluralKey(CustomResourceDefinitionContext c) { + return pluralKey(c.getGroup(), c.getVersion(), c.getPlural()); + } + + private static List pluralKey(String api, String version, String plural) { + return Arrays.asList(api, version, plural); } private HttpUrl parseUrlFromPathAndQuery(String s) { @@ -102,7 +112,7 @@ private HttpUrl parseUrlFromPathAndQuery(String s) { } /** - * Get the name, namespace, and kind from the path + * Get the name, namespace, api, version, plural, and kind from the path */ public Map fromKubernetesPath(String s) { if (s == null || s.isEmpty()) { @@ -170,6 +180,19 @@ public AttributeSet extract(String s) { @Override public AttributeSet extract(HasMetadata hasMetadata) { AttributeSet metadataAttributes = new AttributeSet(); + String apiVersion = hasMetadata.getApiVersion(); + String api = null; + String version = null; + if (!Utils.isNullOrEmpty(apiVersion)) { + api = ApiVersionUtil.trimGroup(apiVersion); + version = ApiVersionUtil.trimVersion(apiVersion); + if (!api.equals(apiVersion)) { + metadataAttributes = metadataAttributes.add(new Attribute(API, api)); + } else { + api = null; + } + metadataAttributes = metadataAttributes.add(new Attribute(VERSION, version)); + } if (!Utils.isNullOrEmpty(hasMetadata.getMetadata().getName())) { metadataAttributes = metadataAttributes.add(new Attribute(NAME, hasMetadata.getMetadata().getName())); } @@ -183,25 +206,42 @@ public AttributeSet extract(HasMetadata hasMetadata) { metadataAttributes = metadataAttributes.add(new Attribute(LABEL_KEY_PREFIX + label.getKey(), label.getValue())); } } - if (!Utils.isNullOrEmpty(hasMetadata.getKind())) { - String kind = hasMetadata.getKind().toLowerCase(Locale.ROOT); - metadataAttributes = metadataAttributes.add(new Attribute(KIND, kind)); - if (hasMetadata instanceof GenericKubernetesResource) { - pluralToKind.put(getPluralForKind(hasMetadata.getKind(), hasMetadata.getApiVersion()), kind); - } else { - pluralToKind.put(hasMetadata.getPlural(), kind); - } + if (!(hasMetadata instanceof GenericKubernetesResource)) { + metadataAttributes = metadataAttributes.add(new Attribute(PLURAL, hasMetadata.getPlural())); + } else { + Optional context = findCrd(api, version); + if (context.isPresent()) { + metadataAttributes = metadataAttributes.add(new Attribute(PLURAL, context.get().getPlural())); + } // else we shouldn't infer the plural without a crd registered - it will come from the path instead } return metadataAttributes; } + private Optional findCrd(String api, String version) { + return crdContexts.values() + .stream() + .filter(c -> Objects.equals(api, c.getGroup()) && Objects.equals(version, c.getVersion())) + .findFirst(); + } + private Map extract(Matcher m) { Map attributes = new HashMap<>(); if (m.matches()) { + + String api = m.group(1); + if (api != null) { + api = api.substring(2); + attributes.put(API, api); + } + + String version = m.group(VERSION); + if (!Utils.isNullOrEmpty(version)) { + attributes.put(VERSION, version); + } + String kind = m.group(KIND); if (!Utils.isNullOrEmpty(kind)) { - kind = resolveKindFromPlural(kind); - attributes.put(KIND, kind); + attributes.put(PLURAL, kind); } String namespace = m.group(NAMESPACE); @@ -221,31 +261,6 @@ private Map extract(Matcher m) { return attributes; } - private String resolveKindFromPlural(String plural) { - String result = getCustomResourceKindFromPlural(plural); - if (result != null) { - return result; - } - return pluralToKind.getOrDefault(plural, plural.substring(0, plural.length() - 1)); - } - - /** - * Find the plural for standard types by consulting the deserializer - */ - private static String getPluralForKind(String kind, String apiVersion) { - GenericKubernetesResource gkr = new GenericKubernetesResource(); - gkr.setApiVersion(apiVersion); - gkr.setKind(kind); - try { - HasMetadata result = Serialization.unmarshal(new ByteArrayInputStream(Serialization.asJson(gkr).getBytes(StandardCharsets.UTF_8))); - if (result != null) { - return result.getPlural(); - } - } catch (KubernetesClientException e) { - } - return kind + "s"; - } - private static AttributeSet extractQueryParameters(HttpUrl url) { AttributeSet attributes = new AttributeSet(); String labelSelector = url.queryParameter("labelSelector"); @@ -287,23 +302,23 @@ private static Attribute parseLabel(String label) { return null; } - static GenericKubernetesResource toKubernetesResource(String s) { - try (InputStream stream = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))) { - return Serialization.unmarshal(stream, GenericKubernetesResource.class); - } catch (IOException e) { - throw new RuntimeException(e); // unexpected + static HasMetadata toKubernetesResource(String s) { + HasMetadata result = Serialization.unmarshal(s); + if (result == null) { + throw new IllegalArgumentException("Required value: kind and apiVersion are required"); } + return result; } - private String getCustomResourceKindFromPlural(String plural) { - CustomResourceDefinitionContext crdContext = crdContexts.get(plural); - return crdContext != null && crdContext.getKind() != null ? crdContext.getKind().toLowerCase(Locale.ROOT) : null; + public CustomResourceDefinitionContext getCrdContext(String api, String version, String plural) { + return this.crdContexts.get(pluralKey(api, version, plural)); } - /** - * A mapping of plural name to context - */ - public Map getCrdContexts() { - return crdContexts; + public void removeCrdContext(CustomResourceDefinitionContext context) { + this.crdContexts.remove(pluralKey(context)); + } + + public void addCrdContext(CustomResourceDefinitionContext context) { + this.crdContexts.put(pluralKey(context), context); } } 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 49d712d7c65..ee97b67aa95 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 @@ -18,9 +18,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.fabric8.kubernetes.api.model.GenericKubernetesResource; import io.fabric8.kubernetes.api.model.HasMetadata; -import io.fabric8.kubernetes.api.model.ObjectMeta; import io.fabric8.kubernetes.api.model.Status; import io.fabric8.kubernetes.api.model.StatusBuilder; import io.fabric8.kubernetes.api.model.StatusCause; @@ -28,6 +26,7 @@ import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; import io.fabric8.kubernetes.client.dsl.base.OperationSupport; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Utils; import io.fabric8.mockwebserver.Context; @@ -126,7 +125,7 @@ public synchronized MockResponse dispatch(RecordedRequest request) { */ @Override public MockResponse handleCreate(String path, String s) { - return validateRequestBodyAndHandleRequest(s, g -> doCreateOrModify(path, g, ADDED)); + return validateRequestBodyAndHandleRequest(s, h -> doCreateOrModify(path, s, h, ADDED)); } /** @@ -136,7 +135,7 @@ public MockResponse handleCreate(String path, String s) { * @return The {@link MockResponse} */ public MockResponse handleReplace(String path, String s) { - return validateRequestBodyAndHandleRequest(s, g -> doCreateOrModify(path, g, MODIFIED)); + return validateRequestBodyAndHandleRequest(s, h -> doCreateOrModify(path, s, h, MODIFIED)); } /** @@ -199,7 +198,7 @@ public MockResponse handlePatch(String path, String s, String contentType) { Map pathValues = kubernetesAttributesExtractor.fromKubernetesPath(path); boolean statusSubresource = - crdProcessor.isStatusSubresource(pathValues.get(KubernetesAttributesExtractor.KIND)); + crdProcessor.isStatusSubresource(pathValues); if (statusSubresource && !isStatusPath(path)) { status = removeStatus(source); @@ -228,15 +227,17 @@ public MockResponse handlePatch(String path, String s, String contentType) { } } - String updatedAsString = context.getMapper().writeValueAsString(updated); - GenericKubernetesResource resource = toKubernetesResource(updatedAsString); - GenericKubernetesResource existingResource = toKubernetesResource(body); - setDefaultMetadata(resource, pathValues, existingResource.getMetadata()); + setDefaultMetadata(updated, pathValues, source); - processEvent(path, bodyEntry.get().getKey(), Serialization.asJson(resource)); + String updatedAsString = Serialization.asJson(updated); - response.setResponseCode(HttpURLConnection.HTTP_ACCEPTED); - response.setBody(updatedAsString); + return validateRequestBodyAndHandleRequest(updatedAsString, h -> { + processEvent(path, query, bodyEntry.get().getKey(), updatedAsString); + + response.setResponseCode(HttpURLConnection.HTTP_ACCEPTED); + response.setBody(updatedAsString); + return response; + }); } catch (JsonProcessingException e) { response.setResponseCode(HTTP_UNPROCESSABLE_ENTITY); } @@ -317,19 +318,24 @@ private String fetchResourceNameFromWatchRequestPath(String path) { } private int doDelete(String path) { - List items = findItems(attributeExtractor.fromPath(path)); + AttributeSet fromPath = attributeExtractor.fromPath(path); + List items = findItems(fromPath); if (items.isEmpty()) return HttpURLConnection.HTTP_NOT_FOUND; - items.forEach(item -> processEvent(path, item, null)); + items.forEach(item -> processEvent(path, fromPath, item, null)); return HttpURLConnection.HTTP_OK; } - private void processEvent(String path, AttributeSet oldAttributes, String newState) { + private void processEvent(String path, AttributeSet pathAttributes, AttributeSet oldAttributes, String newState) { String existing = map.remove(oldAttributes); AttributeSet newAttributes = null; if (newState != null) { newAttributes = kubernetesAttributesExtractor.fromResource(newState); + // corner case - we need to get the plural from the path + if (!newAttributes.containsKey(KubernetesAttributesExtractor.PLURAL)) { + newAttributes = AttributeSet.merge(pathAttributes, newAttributes); + } map.put(newAttributes, newState); } if (!Objects.equals(existing, newState)) { @@ -357,7 +363,7 @@ private List findItems(AttributeSet query) { .collect(Collectors.toList()); } - private MockResponse doCreateOrModify(String path, GenericKubernetesResource value, String event) { + private MockResponse doCreateOrModify(String path, String initial, HasMetadata value, String event) { MockResponse mockResponse = new MockResponse(); // workaround for mockserver https://github.com/fabric8io/mockwebserver/pull/59 Map pathValues = kubernetesAttributesExtractor.fromKubernetesPath(path); @@ -366,13 +372,13 @@ private MockResponse doCreateOrModify(String path, GenericKubernetesResource val try { int responseCode = HttpURLConnection.HTTP_OK; - if (ADDED.equals(event) && value.getMetadata() != null && value.getMetadata().getName() != null) { - attributes = AttributeSet.merge(attributes, new AttributeSet(new Attribute(KubernetesAttributesExtractor.NAME, value.getMetadata().getName()))); + if (ADDED.equals(event)) { + attributes = AttributeSet.merge(attributes, new AttributeSet(new Attribute(KubernetesAttributesExtractor.NAME, KubernetesResourceUtil.getName(value)))); } - boolean statusSubresource = crdProcessor.isStatusSubresource(pathValues.get(KubernetesAttributesExtractor.KIND)); + boolean statusSubresource = crdProcessor.isStatusSubresource(pathValues); - GenericKubernetesResource updated = Serialization.clone(value); + JsonNode updated = context.getMapper().readTree(initial); AttributeSet existingAttributes = null; List items = findItems(attributes); @@ -381,7 +387,7 @@ private MockResponse doCreateOrModify(String path, GenericKubernetesResource val responseCode = HttpURLConnection.HTTP_NOT_FOUND; } else { if (statusSubresource) { - updated.getAdditionalProperties().remove(STATUS); + removeStatus(updated); } setDefaultMetadata(updated, pathValues, null); } @@ -390,31 +396,31 @@ private MockResponse doCreateOrModify(String path, GenericKubernetesResource val } else if (MODIFIED.equals(event)) { existingAttributes = items.get(0); String existing = map.get(existingAttributes); - GenericKubernetesResource existingResource = toKubernetesResource(existing); - Object status = null; + JsonNode existingNode = context.getMapper().readTree(existing); + JsonNode status = null; if (isStatusPath(path)) { - status = updated.getAdditionalProperties().remove(STATUS); + status = removeStatus(updated); // set the status on the existing node - updated = Serialization.clone(existingResource); + updated = existingNode; } else { // preserve status and generated fields if (statusSubresource) { - status = existingResource.getAdditionalProperties().remove(STATUS); + status = removeStatus(existingNode); } - setDefaultMetadata(updated, pathValues, existingResource.getMetadata()); + setDefaultMetadata(updated, pathValues, existingNode); } if (statusSubresource || isStatusPath(path)) { if (status != null) { - updated.getAdditionalProperties().put(STATUS, status); + ((ObjectNode) updated).set(STATUS, status); } else { - updated.getAdditionalProperties().remove(STATUS); + ((ObjectNode) updated).remove(STATUS); } } } if (responseCode == HttpURLConnection.HTTP_OK) { String s = context.getMapper().writeValueAsString(updated); - processEvent(path, existingAttributes, s); + processEvent(path, attributes, existingAttributes, s); mockResponse.setBody(s); } mockResponse.setResponseCode(responseCode); @@ -428,31 +434,31 @@ private static boolean isStatusPath(String path) { return path.endsWith("/" + STATUS); } - private void setDefaultMetadata(GenericKubernetesResource source, Map pathValues, ObjectMeta exitingMetadata) { - ObjectMeta metadata = source.getMetadata(); - if (metadata == null) { - metadata = new ObjectMeta(); - source.setMetadata(metadata); + private void setDefaultMetadata(JsonNode source, Map pathValues, JsonNode existing) { + ObjectNode metadata = (ObjectNode)source.findValue("metadata"); + ObjectNode existingMetadata = null; + if (existing != null) { + existingMetadata = (ObjectNode)existing.findValue("metadata"); } UUID uuid = UUID.randomUUID(); - if (metadata.getName() == null) { - metadata.setName(metadata.getGenerateName() + "-" + uuid.toString()); + if (metadata.get("name") == null) { + metadata.put("name", metadata.get("generateName").asText() + "-" + uuid.toString()); } - if (metadata.getNamespace() == null) { - metadata.setNamespace(pathValues.get(KubernetesAttributesExtractor.NAMESPACE)); + if (metadata.get("namespace") == null) { + metadata.put("namespace", pathValues.get(KubernetesAttributesExtractor.NAMESPACE)); } - metadata.setUid(getOrDefault(exitingMetadata, ObjectMeta::getUid, uuid.toString())); + metadata.put("uid", getOrDefault(existingMetadata, "uid", uuid.toString())); // resourceVersion is not yet handled appropriately - metadata.setResourceVersion("1"); - metadata.setGeneration(1l); - metadata.setCreationTimestamp(getOrDefault(exitingMetadata, ObjectMeta::getCreationTimestamp, ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT))); + metadata.put("resourceVersion", "1"); + metadata.put("generation", 1); + metadata.put("creationTimestamp", getOrDefault(existingMetadata, "creationTimestamp", ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT))); } - private String getOrDefault(ObjectMeta metadata, Function extractor, String defaultValue) { - if (metadata != null) { - String result = extractor.apply(metadata); - if (result != null) { - return result; + private String getOrDefault(JsonNode node, String name, String defaultValue) { + if (node != null) { + JsonNode field = node.get(name); + if (field != null) { + return field.asText(); } } return defaultValue; @@ -462,14 +468,15 @@ private JsonNode removeStatus(JsonNode source) { return ((ObjectNode)source).remove(STATUS); } - private MockResponse validateRequestBodyAndHandleRequest(String s, Function mockResponseFunction) { - GenericKubernetesResource g = null; + // eventually this should validate against the path + private MockResponse validateRequestBodyAndHandleRequest(String s, Function mockResponseFunction) { + HasMetadata h = null; try { - g = toKubernetesResource(s); - validateResource(g); - return mockResponseFunction.apply(g); + h = toKubernetesResource(s); + validateResource(h); + return mockResponseFunction.apply(h); } catch (IllegalArgumentException | KubernetesClientException e) { - return getUnprocessableEntityMockResponse(s, g, e); + return getUnprocessableEntityMockResponse(s, h, e); } } @@ -519,5 +526,11 @@ private void validateResource(HasMetadata item) { if (Utils.isNullOrEmpty(item.getMetadata().getName()) && Utils.isNullOrEmpty(item.getMetadata().getGenerateName())) { throw new IllegalArgumentException("Required value: name or generateName is required"); } + if (Utils.isNullOrEmpty(item.getKind())) { + throw new IllegalArgumentException("Required value: kind is required"); + } + if (Utils.isNullOrEmpty(item.getApiVersion())) { + throw new IllegalArgumentException("Required value: apiVersion is required"); + } } } diff --git a/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractorTest.java b/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractorTest.java index 69d5bf89d98..f28c864e0f7 100644 --- a/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractorTest.java +++ b/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/KubernetesAttributesExtractorTest.java @@ -19,14 +19,18 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import io.fabric8.kubernetes.client.dsl.base.CustomResourceDefinitionContext; +import io.fabric8.kubernetes.client.utils.Serialization; import org.junit.jupiter.api.Test; -import io.fabric8.kubernetes.api.model.EndpointsBuilder; +import io.fabric8.kubernetes.api.model.GenericKubernetesResource; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.fabric8.kubernetes.api.model.Pod; import io.fabric8.kubernetes.api.model.PodBuilder; import io.fabric8.kubernetes.api.model.apps.Deployment; @@ -44,10 +48,12 @@ public void shouldHandleNamespacedPathWithResource() { AttributeSet attributes = extractor.fromPath("/api/v1/namespaces/myns/pods/mypod"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "pod")); + expected = expected.add(new Attribute("plural", "pods")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "mypod")); + expected = expected.add(new Attribute("version", "v1")); assertTrue(attributes.matches(expected)); + assertFalse(attributes.containsKey(KubernetesAttributesExtractor.API)); } @Test @@ -56,7 +62,7 @@ public void shouldHandleNamespacedPath() { AttributeSet attributes = extractor.fromPath("/api/v1/namespaces/myns/pods"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "pod")); + expected = expected.add(new Attribute("plural", "pods")); expected = expected.add(new Attribute("namespace", "myns")); assertTrue(attributes.matches(expected)); } @@ -67,19 +73,19 @@ public void shouldHandleNonNamespacedPath() { AttributeSet attributes = extractor.fromPath("/api/v1/nodes/mynode"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "node")); + expected = expected.add(new Attribute("plural", "nodes")); expected = expected.add(new Attribute("name", "mynode")); assertTrue(attributes.matches(expected)); } @Test public void shouldHandlePathWithParameters() { - KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); - AttributeSet attributes = extractor.fromPath("/api/v1/pods?labelSelector=testKey%3DtestValue"); + KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); + AttributeSet attributes = extractor.fromPath("/api/v1/pods?labelSelector=testKey%3DtestValue"); - AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "pod")); - assertTrue(attributes.matches(expected)); + AttributeSet expected = new AttributeSet(); + expected = expected.add(new Attribute("plural", "pods")); + assertTrue(attributes.matches(expected)); } @Test @@ -90,10 +96,12 @@ public void shouldHandleResource() { AttributeSet attributes = extractor.extract(pod); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "pod")); + expected = expected.add(new Attribute("plural", "pods")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "mypod")); + expected = expected.add(new Attribute("version", "v1")); assertTrue(attributes.matches(expected)); + assertFalse(attributes.containsKey(KubernetesAttributesExtractor.API)); } @Test @@ -104,10 +112,11 @@ void shouldHandleRawResource() { AttributeSet attributes = extractor.extract(resource); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "raw")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "myresource")); + expected = expected.add(new Attribute("version", "v1")); assertTrue(attributes.matches(expected)); + assertFalse(attributes.containsKey(KubernetesAttributesExtractor.API)); } @Test @@ -124,13 +133,16 @@ public void shouldHandleResourceWithLabel() { assertTrue(attributes.matches(expected)); } + /** + * Default versions are not yet understood + */ @Test public void shouldHandleKindWithoutVersion() { KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); AttributeSet attributes = extractor.fromPath("/api/pods"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "pod")); + expected = expected.add(new Attribute("plural", "pods")); assertTrue(attributes.matches(expected)); } @@ -140,7 +152,9 @@ public void shouldHandleExtensions() { AttributeSet attributes = extractor.fromPath("/apis/apps/v1/deployments"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "deployment")); + expected = expected.add(new Attribute("plural", "deployments")); + expected = expected.add(new Attribute("api", "apps")); + expected = expected.add(new Attribute("version", "v1")); assertTrue(attributes.matches(expected)); } @@ -151,7 +165,7 @@ public void shouldHandleIngress() { AttributeSet attributes = extractor.fromPath("/apis/extensions/v1beta1/namespaces/myns/ingresses/myingress"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "ingress")); + expected = expected.add(new Attribute("plural", "ingresses")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "myingress")); assertTrue(attributes.matches(expected)); @@ -160,11 +174,10 @@ public void shouldHandleIngress() { @Test public void shouldHandleEndpoints() { KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); - extractor.extract(new EndpointsBuilder().withNewMetadata().endMetadata().build()); AttributeSet attributes = extractor.fromPath("/api/v1/namespaces/myns/endpoints"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "endpoints")); + expected = expected.add(new Attribute("plural", "endpoints")); expected = expected.add(new Attribute("namespace", "myns")); assertTrue(attributes.matches(expected)); } @@ -172,11 +185,10 @@ public void shouldHandleEndpoints() { @Test public void shouldHandleIngresses() { KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); - extractor.extract(new IngressBuilder().withNewMetadata().endMetadata().build()); AttributeSet attributes = extractor.fromPath("/apis/extensions/v1beta1/namespaces/myns/ingresses"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "ingress")); + expected = expected.add(new Attribute("plural", "ingresses")); expected = expected.add(new Attribute("namespace", "myns")); assertTrue(attributes.matches(expected)); } @@ -188,27 +200,59 @@ public void shouldHandleApiGroups() { .fromPath("/apis/autoscaling/v1/namespaces/myns/horizontalpodautoscalers/myhpa"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "horizontalpodautoscaler")); + expected = expected.add(new Attribute("plural", "horizontalpodautoscalers")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "myhpa")); assertTrue(attributes.matches(expected)); } @Test - public void shouldHandleCrds() { + public void shouldHandleCrdsTypeUnknown() { KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); AttributeSet attributes = extractor.fromPath("/apis/test.com/v1/namespaces/myns/crds/mycrd"); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "crd")); + expected = expected.add(new Attribute("plural", "crds")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "mycrd")); + expected = expected.add(new Attribute("version", "v1")); + expected = expected.add(new Attribute("api", "test.com")); assertTrue(attributes.matches(expected)); } + @Test + public void shouldHandleCrds() { + CustomResourceDefinitionContext crdContext = new CustomResourceDefinitionContext.Builder() + .withScope("Namespaced") + .withPlural("crds") + .withVersion("v1") + .withGroup("test.com") + .withKind("crd") + .build(); + + KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(Arrays.asList(crdContext)); + AttributeSet attributes = extractor.fromPath("/apis/test.com/v1/namespaces/myns/crds/mycrd"); + + AttributeSet expected = new AttributeSet(); + expected = expected.add(new Attribute("plural", "crds")); + expected = expected.add(new Attribute("namespace", "myns")); + expected = expected.add(new Attribute("name", "mycrd")); + expected = expected.add(new Attribute("api", "test.com")); + expected = expected.add(new Attribute("version", "v1")); + assertTrue(attributes.matches(expected)); + } + @Test void shouldHandleCrdSubresources() { - KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); + CustomResourceDefinitionContext crdContext = new CustomResourceDefinitionContext.Builder() + .withScope("Namespaced") + .withPlural("crds") + .withVersion("v1") + .withGroup("test.com") + .withKind("crd") + .build(); + + KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(Arrays.asList(crdContext)); String[] subresources = new String[]{"status", "scale"}; String basePath = "/apis/test.com/v1/namespaces/myns/crds/mycrd/"; @@ -216,7 +260,7 @@ void shouldHandleCrdSubresources() { AttributeSet attributes = extractor.fromPath(basePath + subresource); AttributeSet expected = new AttributeSet(); - expected = expected.add(new Attribute("kind", "crd")); + expected = expected.add(new Attribute("plural", "crds")); expected = expected.add(new Attribute("namespace", "myns")); expected = expected.add(new Attribute("name", "mycrd")); assertTrue(attributes.matches(expected), @@ -416,7 +460,63 @@ void testCustomResourceAttributesExtraction() { // Then AttributeSet expected = new AttributeSet(); expected = expected.add(new Attribute("namespace", "ns1")); - expected = expected.add(new Attribute("kind", "customdatabase")); + expected = expected.add(new Attribute("plural", "customdatabases")); + expected = expected.add(new Attribute("api", "demo.fabric8.io")); + expected = expected.add(new Attribute("version", "v1alpha1")); assertTrue(attributes.matches(expected)); } + + @Test + public void kubernetesPathIngresses() { + KubernetesAttributesExtractor extractor = new KubernetesAttributesExtractor(); + Map attributes = extractor.fromKubernetesPath("/apis/extensions/v1beta1/namespaces/myns/ingresses/myingress"); + + assertEquals("ingresses", attributes.get(KubernetesAttributesExtractor.PLURAL)); + } + + @Test + public void testMultipleCrdVersions() throws IOException { + helpTestMultipleCrdVersions(true); + } + + @Test + public void testMultipleCrdVersionsUnregistered() throws IOException { + helpTestMultipleCrdVersions(false); + } + + private void helpTestMultipleCrdVersions(boolean registered) throws IOException { + CustomResourceDefinitionContext crdContextV1 = new CustomResourceDefinitionContext.Builder() + .withScope("Namespaced") + .withPlural("customdatabases") + .withVersion("v1") + .withGroup("demo.fabric8.io") + .withKind("CustomDatabase") + .build(); + + CustomResourceDefinitionContext crdContextV1Alpha1 = new CustomResourceDefinitionContext.Builder() + .withScope("Namespaced") + .withPlural("customdatabases") + .withVersion("v1alpha1") + .withGroup("demo.fabric8.io") + .withKind("CustomDatabase") + .build(); + + KubernetesServer kubernetesServer = new KubernetesServer(false, true, + registered ? Arrays.asList(crdContextV1, crdContextV1Alpha1) : Collections.emptyList()); + kubernetesServer.before(); + KubernetesClient kubernetesClient = kubernetesServer.getClient(); + + GenericKubernetesResource gkr = new GenericKubernetesResource(); + gkr.setMetadata(new ObjectMetaBuilder().withName("v1").build()); + gkr.setKind("CustomDatabase"); + gkr.setApiVersion("demo.fabric8.io/v1"); + + kubernetesClient.customResource(crdContextV1).create(Serialization.asJson(gkr)); + + gkr.setApiVersion("demo.fabric8.io/v1alpha1"); + kubernetesClient.customResource(crdContextV1Alpha1).create(Serialization.asJson(gkr)); + + assertEquals(1, (((List)kubernetesClient.customResource(crdContextV1).list().get("items")).size())); + assertEquals(1, (((List)kubernetesClient.customResource(crdContextV1Alpha1).list().get("items")).size())); + } }