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..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 @@ -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,123 @@ 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) + && 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 + 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); + + // **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: " + + 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, modelListClass); + } 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 + * @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 + */ + public static Object submitResourceToApi( + ApiType resource, + Class apiTypeClass, + String group, + String version, + String resourcePlural, + Class apiListTypeClass) throws Exception { // **Change**: Added apiListTypeClass + + // Creating an API client and configuring it + ApiClient client = Config.defaultClient(); + Configuration.setDefaultApiClient(client); + + // Configuring the GenericKubernetesApi handler with explicit list type + 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..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 @@ -15,21 +15,30 @@ 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.ModelMapper; +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 +115,58 @@ 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); + 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); + + 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"; + Class listTypeClass = V1Pod.class; // Mock list type class for Pod list + + Exception exception = assertThrows(RuntimeException.class, () -> { + KubectlCreate.submitResourceToApi(pod, V1Pod.class, group, version, resourcePlural, listTypeClass); + }); + + assertThat(exception.getMessage()).contains("Failed to create resource"); + } }