From 8140b314bbb3a7a37a8291ba82f7c170727fbf3c Mon Sep 17 00:00:00 2001 From: vaidikcode Date: Thu, 24 Oct 2024 06:52:48 +0530 Subject: [PATCH 1/5] final commit --- .../extended/kubectl/KubectlCreate.java | 127 ++++++++++++++++++ .../extended/kubectl/KubectlCreateTest.java | 54 ++++++++ 2 files changed, 181 insertions(+) diff --git a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java index f0c1a6cfcb..5ba4b7bbd3 100644 --- a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java +++ b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java @@ -12,16 +12,29 @@ */ package io.kubernetes.client.extended.kubectl; +import io.kubernetes.client.Discovery; import io.kubernetes.client.common.KubernetesListObject; import io.kubernetes.client.common.KubernetesObject; import io.kubernetes.client.extended.kubectl.exception.KubectlException; +import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.apis.CustomObjectsApi; import io.kubernetes.client.util.ModelMapper; import io.kubernetes.client.util.Namespaces; import io.kubernetes.client.util.Strings; +import io.kubernetes.client.util.Yaml; +import io.kubernetes.client.util.Config; import io.kubernetes.client.util.generic.GenericKubernetesApi; +import io.kubernetes.client.util.generic.KubernetesApiResponse; import io.kubernetes.client.util.generic.options.CreateOptions; +import java.io.Reader; +import java.text.ParseException; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + public class KubectlCreate extends Kubectl.ResourceBuilder> implements Kubectl.Executable { @@ -65,4 +78,118 @@ public ApiType execute() throws KubectlException { } } } + /** + * Loads an API object from a YAML string representation without knowing the object type + * and submits any type of resource. + * + * Note: This method works for single-instance YAML files only. + * If the YAML contains multiple resources, it will fail. + * Support for multiple resources in a single YAML file is not implemented yet. + * + * @param content The YAML content as a Reader + * @return The created resource object + * @throws Exception If there is an error in parsing and submitting resources. + */ + public static Object loadAndSubmitResource(Reader content) throws Exception { + // Initialize the API client and Discovery instance + ApiClient client = Configuration.getDefaultApiClient(); + Discovery discovery = new Discovery(client); + + // Load the YAML content as an unstructured object + Object unstructuredObject = Yaml.load(content); + + // Ensure the unstructured object is a Map + if (!(unstructuredObject instanceof Map)) { + throw new ParseException("Invalid YAML", 0); + } + + // Typecasting the unstructured object to a Map + Map yamlMap = (Map) unstructuredObject; + + // Getting apiVersion and kind + String apiVersion = (String) yamlMap.get("apiVersion"); + String kind = (String) yamlMap.get("kind"); + + // Validate apiVersion and kind + if (apiVersion == null || kind == null) { + throw new ParseException("YAML does not contain apiVersion or kind", 0); + } + + // Finding the resource in the discovery class + Set resources = discovery.findAll(); + Optional apiResource = resources.stream() + .filter(r -> r.getKind().equalsIgnoreCase(kind)) + .findFirst(); + + // Check if the resource kind was found + if (apiResource.isPresent()) { + String group = apiResource.get().getGroup(); + String version = apiResource.get().getPreferredVersion(); + String resourcePlural = apiResource.get().getResourcePlural(); + + // Getting the strongly typed class from ModelMapper + Class modelClass = ModelMapper.getApiTypeClass(group, version, kind); + + // Validate model class + if (modelClass == null) { + throw new IllegalArgumentException("Could not determine model class for group: " + + group + ", version: " + version + ", kind: " + kind); + } + + // Reload the YAML into a strongly typed object + KubernetesObject typedObject = (KubernetesObject) Yaml.loadAs(content, modelClass); + + // Submit the resource to Kubernetes API + return submitResourceToApi(typedObject, (Class) modelClass, group, version, resourcePlural); + } else { + throw new IllegalArgumentException("Invalid resource kind: " + kind); + } + } + + /** + * Submits any resource to Kubernetes API. + * + * @param resource The resource to be submitted + * @param apiTypeClass The class type of the API resource + * @param group The API group of the resource + * @param version The API version of the resource + * @param resourcePlural The plural form of the resource + * @return The created resource object from the API response + * @throws Exception If there is an error in submitting the resource + */ + public static Object submitResourceToApi( + ApiType resource, + Class apiTypeClass, + String group, + String version, + String resourcePlural) throws Exception { + + // Creating an API client and configuring it + ApiClient client = Config.defaultClient(); + Configuration.setDefaultApiClient(client); + + // Using the apiTypeClass and apiListTypeClass for list of resources + Class apiListTypeClass = ModelMapper.getApiTypeClass(group, version, resourcePlural); + + // Configuring the GenericKubernetesApi handler + GenericKubernetesApi genericApi = new GenericKubernetesApi<>( + apiTypeClass, + apiListTypeClass, + group, + version, + resourcePlural, + new CustomObjectsApi(client) + ); + + // Creating the resource in Kubernetes + KubernetesApiResponse apiResponse = genericApi.create(resource); + + // Check if the resource creation was successful + if (!apiResponse.isSuccess()) { + throw new RuntimeException("Failed to create resource: " + apiResponse.getStatus()); + } + + // Return the created resource + return apiResponse.getObject(); + } } diff --git a/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java b/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java index b24eef80a6..55218032b6 100644 --- a/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java +++ b/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java @@ -15,21 +15,29 @@ import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.github.tomakehurst.wiremock.junit5.WireMockExtension; import io.kubernetes.client.extended.kubectl.exception.KubectlException; import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1ObjectMeta; +import io.kubernetes.client.openapi.models.V1Pod; import io.kubernetes.client.util.ClientBuilder; import java.io.File; import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; + +import io.kubernetes.client.util.Yaml; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.MockedStatic; +import org.mockito.Mockito; class KubectlCreateTest { @@ -106,4 +114,50 @@ void createConfigMap() throws KubectlException, IOException { apiServer.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/namespaces/foo/configmaps"))); assertThat(configMap).isNotNull(); } + @Test + void testLoadAndSubmitResourceSuccess() throws Exception { + String yamlContent = + "apiVersion: v1\n" + + "kind: Pod\n" + + "metadata:\n" + + " name: test-pod\n"; + + Reader reader = new StringReader(yamlContent); + + try (MockedStatic yamlMock = Mockito.mockStatic(Yaml.class)) { + V1Pod mockPod = new V1Pod(); + yamlMock.when(() -> Yaml.load(reader)).thenReturn(mockPod); + + Object result = KubectlCreate.loadAndSubmitResource(reader); + + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(V1Pod.class); + } + } + + @Test + void testLoadAndSubmitResourceInvalidYaml() { + String invalidYamlContent = "invalid: yaml"; + Reader reader = new StringReader(invalidYamlContent); + + Exception exception = assertThrows(Exception.class, () -> { + KubectlCreate.loadAndSubmitResource(reader); + }); + + assertThat(exception.getMessage()).contains("Invalid YAML"); + } + + @Test + void testSubmitResourceToApiFailure() throws Exception { + V1Pod pod = new V1Pod(); + String group = "apps"; + String version = "v1"; + String resourcePlural = "pods"; + + Exception exception = assertThrows(RuntimeException.class, () -> { + KubectlCreate.submitResourceToApi(pod, V1Pod.class, group, version, resourcePlural); + }); + + assertThat(exception.getMessage()).contains("Failed to create resource"); + } } From 5c39eb2898339f7bcee7a35402d1daf411dc56ad Mon Sep 17 00:00:00 2001 From: vaidikcode Date: Thu, 24 Oct 2024 07:11:51 +0530 Subject: [PATCH 2/5] final commit --- .../io/kubernetes/client/extended/kubectl/KubectlCreate.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java index 5ba4b7bbd3..a6b26f9fc3 100644 --- a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java +++ b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java @@ -118,7 +118,8 @@ public static Object loadAndSubmitResource(Reader content) throws Exception { // Finding the resource in the discovery class Set resources = discovery.findAll(); Optional apiResource = resources.stream() - .filter(r -> r.getKind().equalsIgnoreCase(kind)) + .filter(r -> r.getKind().equalsIgnoreCase(kind) + && r.getVersions().stream().anyMatch(v -> v.equalsIgnoreCase(apiVersion))) .findFirst(); // Check if the resource kind was found From 59525f22d5bd56410a639f6fa225deb2131ea8ba Mon Sep 17 00:00:00 2001 From: vaidikcode Date: Thu, 24 Oct 2024 07:18:57 +0530 Subject: [PATCH 3/5] final commit --- .../io/kubernetes/client/extended/kubectl/KubectlCreate.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java index a6b26f9fc3..d8d928c923 100644 --- a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java +++ b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java @@ -119,7 +119,8 @@ public static Object loadAndSubmitResource(Reader content) throws Exception { Set resources = discovery.findAll(); Optional apiResource = resources.stream() .filter(r -> r.getKind().equalsIgnoreCase(kind) - && r.getVersions().stream().anyMatch(v -> v.equalsIgnoreCase(apiVersion))) + && r.getVersions().stream().anyMatch(v -> v.equalsIgnoreCase(apiVersion)) + && (r.getGroup() == null || r.getGroup().equalsIgnoreCase((String) yamlMap.get("group")))) .findFirst(); // Check if the resource kind was found From d0b223268b0a4e45402859ed69d251bda5363f6f Mon Sep 17 00:00:00 2001 From: vaidikcode Date: Tue, 29 Oct 2024 23:44:56 +0530 Subject: [PATCH 4/5] final commit --- .../client/extended/kubectl/KubectlCreate.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java index d8d928c923..f8c6605f23 100644 --- a/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java +++ b/extended/src/main/java/io/kubernetes/client/extended/kubectl/KubectlCreate.java @@ -132,6 +132,9 @@ public static Object loadAndSubmitResource(Reader content) throws Exception { // Getting the strongly typed class from ModelMapper Class modelClass = ModelMapper.getApiTypeClass(group, version, kind); + // **Change**: Add check for the list type of the resource. + Class modelListClass = ModelMapper.getApiTypeClass(group, version, kind + "List"); + // Validate model class if (modelClass == null) { throw new IllegalArgumentException("Could not determine model class for group: " @@ -142,7 +145,7 @@ public static Object loadAndSubmitResource(Reader content) throws Exception { KubernetesObject typedObject = (KubernetesObject) Yaml.loadAs(content, modelClass); // Submit the resource to Kubernetes API - return submitResourceToApi(typedObject, (Class) modelClass, group, version, resourcePlural); + return submitResourceToApi(typedObject, (Class) modelClass, group, version, resourcePlural, modelListClass); } else { throw new IllegalArgumentException("Invalid resource kind: " + kind); } @@ -156,6 +159,7 @@ public static Object loadAndSubmitResource(Reader content) throws Exception { * @param group The API group of the resource * @param version The API version of the resource * @param resourcePlural The plural form of the resource + * @param apiListTypeClass The class type for the list of the resource * @return The created resource object from the API response * @throws Exception If there is an error in submitting the resource */ @@ -164,17 +168,15 @@ public static Object submitResourceToApi( Class apiTypeClass, String group, String version, - String resourcePlural) throws Exception { + String resourcePlural, + Class apiListTypeClass) throws Exception { // **Change**: Added apiListTypeClass // Creating an API client and configuring it ApiClient client = Config.defaultClient(); Configuration.setDefaultApiClient(client); - // Using the apiTypeClass and apiListTypeClass for list of resources - Class apiListTypeClass = ModelMapper.getApiTypeClass(group, version, resourcePlural); - - // Configuring the GenericKubernetesApi handler - GenericKubernetesApi genericApi = new GenericKubernetesApi<>( + // Configuring the GenericKubernetesApi handler with explicit list type + GenericKubernetesApi genericApi = new GenericKubernetesApi<>( apiTypeClass, apiListTypeClass, group, @@ -194,4 +196,5 @@ public static Object submitResourceToApi( // Return the created resource return apiResponse.getObject(); } + } From 341414b201b2a0adcd5b171b9b8da94cef2816b6 Mon Sep 17 00:00:00 2001 From: vaidikcode Date: Tue, 29 Oct 2024 23:48:18 +0530 Subject: [PATCH 5/5] final commit --- .../client/extended/kubectl/KubectlCreateTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java b/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java index 55218032b6..3e42ada7a1 100644 --- a/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java +++ b/extended/src/test/java/io/kubernetes/client/extended/kubectl/KubectlCreateTest.java @@ -32,6 +32,7 @@ import java.nio.file.Paths; import java.util.HashMap; +import io.kubernetes.client.util.ModelMapper; import io.kubernetes.client.util.Yaml; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -124,9 +125,16 @@ void testLoadAndSubmitResourceSuccess() throws Exception { Reader reader = new StringReader(yamlContent); - try (MockedStatic yamlMock = Mockito.mockStatic(Yaml.class)) { + try (MockedStatic yamlMock = Mockito.mockStatic(Yaml.class); + MockedStatic modelMapperMock = Mockito.mockStatic(ModelMapper.class)) { + V1Pod mockPod = new V1Pod(); + Class listTypeClass = V1Pod.class; // Mock list type class + + // Mock the Yaml and ModelMapper behaviors yamlMock.when(() -> Yaml.load(reader)).thenReturn(mockPod); + modelMapperMock.when(() -> ModelMapper.getApiTypeClass("v1", "v1", "Pod")).thenReturn(V1Pod.class); + modelMapperMock.when(() -> ModelMapper.getApiTypeClass("v1", "v1", "PodList")).thenReturn(listTypeClass); Object result = KubectlCreate.loadAndSubmitResource(reader); @@ -153,9 +161,10 @@ void testSubmitResourceToApiFailure() throws Exception { String group = "apps"; String version = "v1"; String resourcePlural = "pods"; + Class listTypeClass = V1Pod.class; // Mock list type class for Pod list Exception exception = assertThrows(RuntimeException.class, () -> { - KubectlCreate.submitResourceToApi(pod, V1Pod.class, group, version, resourcePlural); + KubectlCreate.submitResourceToApi(pod, V1Pod.class, group, version, resourcePlural, listTypeClass); }); assertThat(exception.getMessage()).contains("Failed to create resource");