diff --git a/examples/examples-release-latest/src/main/java/io/kubernetes/client/examples/YamlCreateResourceExample.java b/examples/examples-release-latest/src/main/java/io/kubernetes/client/examples/YamlCreateResourceExample.java new file mode 100644 index 0000000000..873d066d46 --- /dev/null +++ b/examples/examples-release-latest/src/main/java/io/kubernetes/client/examples/YamlCreateResourceExample.java @@ -0,0 +1,105 @@ +/* +Copyright 2020 The Kubernetes Authors. +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.kubernetes.client.examples; + +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.Configuration; +import io.kubernetes.client.openapi.apis.CoreV1Api; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.util.Config; +import io.kubernetes.client.util.Yaml; +import java.io.File; +import java.io.IOException; + +/** + * A simple example of how to use Yaml.createResource() to create Kubernetes resources from YAML + * without specifying the type upfront. This is equivalent to `kubectl create -f `. + * + *

Easiest way to run this: mvn exec:java + * -Dexec.mainClass="io.kubernetes.client.examples.YamlCreateResourceExample" + * + *

From inside $REPO_DIR/examples + */ +public class YamlCreateResourceExample { + public static void main(String[] args) throws IOException, ApiException { + // Initialize the API client + ApiClient client = Config.defaultClient(); + Configuration.setDefaultApiClient(client); + + // Example 1: Create a ConfigMap from YAML string + // This method automatically determines the resource type (ConfigMap) + // and uses the appropriate API to create it + String configMapYaml = + "apiVersion: v1\n" + + "kind: ConfigMap\n" + + "metadata:\n" + + " name: example-config\n" + + " namespace: default\n" + + "data:\n" + + " database.url: jdbc:postgresql://localhost/mydb\n" + + " database.user: admin\n"; + + System.out.println("Creating ConfigMap from YAML string..."); + Object configMapResult = Yaml.createResource(client, configMapYaml); + System.out.println("Created: " + configMapResult); + + // Example 2: Create a Pod from YAML string + // Again, no need to specify V1Pod.class - the method determines it automatically + String podYaml = + "apiVersion: v1\n" + + "kind: Pod\n" + + "metadata:\n" + + " name: example-pod\n" + + " namespace: default\n" + + "spec:\n" + + " containers:\n" + + " - name: nginx\n" + + " image: nginx:1.14.2\n" + + " ports:\n" + + " - containerPort: 80\n"; + + System.out.println("\nCreating Pod from YAML string..."); + Object podResult = Yaml.createResource(client, podYaml); + System.out.println("Created: " + podResult); + + // Example 3: Create a resource from a YAML file + // This works with any Kubernetes resource type + File yamlFile = new File("example-resource.yaml"); + if (yamlFile.exists()) { + System.out.println("\nCreating resource from YAML file..."); + Object fileResult = Yaml.createResource(client, yamlFile); + System.out.println("Created: " + fileResult); + } + + // Example 4: Type casting if you need to access specific fields + // The returned object is the strongly-typed Kubernetes object + V1ConfigMap configMap = (V1ConfigMap) configMapResult; + System.out.println("\nConfigMap name: " + configMap.getMetadata().getName()); + System.out.println("ConfigMap data: " + configMap.getData()); + + V1Pod pod = (V1Pod) podResult; + System.out.println("\nPod name: " + pod.getMetadata().getName()); + System.out.println("Pod phase: " + pod.getStatus().getPhase()); + + // Clean up - delete the created resources + CoreV1Api api = new CoreV1Api(); + System.out.println("\nCleaning up..."); + api.deleteNamespacedConfigMap("example-config", "default").execute(); + System.out.println("Deleted ConfigMap"); + + api.deleteNamespacedPod("example-pod", "default").execute(); + System.out.println("Deleted Pod"); + } +} diff --git a/util/src/main/java/io/kubernetes/client/util/Yaml.java b/util/src/main/java/io/kubernetes/client/util/Yaml.java index e8c02999ca..3e1c8f8adc 100644 --- a/util/src/main/java/io/kubernetes/client/util/Yaml.java +++ b/util/src/main/java/io/kubernetes/client/util/Yaml.java @@ -566,4 +566,169 @@ private static Object modelMapper(Map data) throws IOException { public static void addModelMap(String apiGroupVersion, String kind, Class clazz) { ModelMapper.addModelMap(apiGroupVersion, kind, clazz); } + + /** + * Create a Kubernetes resource from a YAML string. This method automatically determines the + * resource type from the YAML content (apiVersion and kind) and uses the appropriate API to + * create the resource. + * + *

This is equivalent to `kubectl create -f `. + * + *

Example usage: + *

{@code
+   * ApiClient client = Config.defaultClient();
+   * String yaml = "apiVersion: v1\n" +
+   *               "kind: ConfigMap\n" +
+   *               "metadata:\n" +
+   *               "  name: my-config\n" +
+   *               "  namespace: default\n";
+   * Object created = Yaml.createResource(client, yaml);
+   * }
+ * + * @param client The API client to use for creating the resource + * @param content The YAML content as a string + * @return The created resource object + * @throws IOException If an error occurs while reading or parsing the YAML + * @throws io.kubernetes.client.openapi.ApiException If an error occurs while creating the resource in the cluster + */ + public static Object createResource(io.kubernetes.client.openapi.ApiClient client, String content) + throws IOException, io.kubernetes.client.openapi.ApiException { + return createResource(client, new StringReader(content)); + } + + /** + * Create a Kubernetes resource from a YAML file. This method automatically determines the + * resource type from the YAML content (apiVersion and kind) and uses the appropriate API to + * create the resource. + * + *

This is equivalent to `kubectl create -f `. + * + * @param client The API client to use for creating the resource + * @param f The YAML file to load + * @return The created resource object + * @throws IOException If an error occurs while reading or parsing the YAML + * @throws io.kubernetes.client.openapi.ApiException If an error occurs while creating the resource in the cluster + */ + public static Object createResource(io.kubernetes.client.openapi.ApiClient client, File f) + throws IOException, io.kubernetes.client.openapi.ApiException { + return createResource(client, new FileReader(f)); + } + + /** + * Create a Kubernetes resource from a YAML stream. This method automatically determines the + * resource type from the YAML content (apiVersion and kind) and uses the appropriate API to + * create the resource. + * + *

This is equivalent to `kubectl create -f `. + * + * @param client The API client to use for creating the resource + * @param reader The stream to load + * @return The created resource object + * @throws IOException If an error occurs while reading or parsing the YAML + * @throws io.kubernetes.client.openapi.ApiException If an error occurs while creating the resource in the cluster + */ + public static Object createResource(io.kubernetes.client.openapi.ApiClient client, Reader reader) + throws IOException, io.kubernetes.client.openapi.ApiException { + // Read the entire content into a string first so we can parse it twice + StringBuilder sb = new StringBuilder(); + char[] buffer = new char[8192]; + int read; + while ((read = reader.read(buffer)) != -1) { + sb.append(buffer, 0, read); + } + String yamlContent = sb.toString(); + + // Load the YAML as a map to extract apiVersion and kind + // Note: The getSnakeYaml() method already configures LoaderOptions with appropriate + // security settings to prevent YAML bombs and other attacks + Map data = getSnakeYaml(null).load(new StringReader(yamlContent)); + + String kind = (String) data.get("kind"); + if (kind == null) { + throw new IOException("Missing kind in YAML!"); + } + String apiVersion = (String) data.get("apiVersion"); + if (apiVersion == null) { + throw new IOException("Missing apiVersion in YAML!"); + } + + // Use ModelMapper to get the appropriate class for this resource type + Class clazz = ModelMapper.getApiTypeClass(apiVersion, kind); + if (clazz == null) { + throw new IOException( + "Unknown apiVersion/kind: " + apiVersion + "/" + kind + ". Is it registered?"); + } + + // Load the YAML into the strongly typed object using the same content + Object resource = loadAs(new StringReader(yamlContent), clazz); + + // Ensure the resource is a KubernetesObject + if (!(resource instanceof io.kubernetes.client.common.KubernetesObject)) { + throw new IOException( + "Resource is not a KubernetesObject: " + resource.getClass().getName()); + } + + io.kubernetes.client.common.KubernetesObject k8sObject = + (io.kubernetes.client.common.KubernetesObject) resource; + + // Parse apiVersion to extract group and version + io.kubernetes.client.apimachinery.GroupVersionKind gvk = + ModelMapper.groupVersionKindFromApiVersionAndKind(apiVersion, kind); + + // Get the resource metadata to determine the plural name + io.kubernetes.client.apimachinery.GroupVersionResource gvr = + ModelMapper.getGroupVersionResourceByClass(clazz); + + if (gvr == null) { + // If no GVR mapping exists, we need to perform discovery + io.kubernetes.client.Discovery discovery = new io.kubernetes.client.Discovery(client); + ModelMapper.refresh(discovery); + gvr = ModelMapper.getGroupVersionResourceByClass(clazz); + + if (gvr == null) { + throw new IOException( + "Unable to determine resource plural name for " + apiVersion + "/" + kind); + } + } + + // Create a GenericKubernetesApi for this resource type + io.kubernetes.client.util.generic.GenericKubernetesApi< + io.kubernetes.client.common.KubernetesObject, + io.kubernetes.client.common.KubernetesListObject> + api = + new io.kubernetes.client.util.generic.GenericKubernetesApi<>( + (Class) clazz, + io.kubernetes.client.common.KubernetesListObject.class, + gvk.getGroup(), + gvk.getVersion(), + gvr.getResource(), + client); + + // Create the resource + io.kubernetes.client.util.generic.KubernetesApiResponse< + io.kubernetes.client.common.KubernetesObject> + response; + + Boolean isNamespaced = ModelMapper.isNamespaced(clazz); + if (isNamespaced != null && isNamespaced) { + // For namespaced resources + String namespace = k8sObject.getMetadata().getNamespace(); + if (namespace == null || namespace.isEmpty()) { + // Default to "default" namespace, matching kubectl behavior + namespace = "default"; + } + response = api.create(namespace, k8sObject, new io.kubernetes.client.util.generic.options.CreateOptions()); + } else { + // For cluster-scoped resources + response = api.create(k8sObject, new io.kubernetes.client.util.generic.options.CreateOptions()); + } + + if (!response.isSuccess()) { + throw new io.kubernetes.client.openapi.ApiException( + response.getHttpStatusCode(), + "Failed to create resource: " + response.getStatus()); + } + + return response.getObject(); + } } diff --git a/util/src/test/java/io/kubernetes/client/util/YamlCreateResourceTest.java b/util/src/test/java/io/kubernetes/client/util/YamlCreateResourceTest.java new file mode 100644 index 0000000000..75169c549c --- /dev/null +++ b/util/src/test/java/io/kubernetes/client/util/YamlCreateResourceTest.java @@ -0,0 +1,252 @@ +/* +Copyright 2020 The Kubernetes Authors. +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.kubernetes.client.util; + +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 com.github.tomakehurst.wiremock.junit5.WireMockExtension; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.openapi.models.V1ConfigMap; +import io.kubernetes.client.openapi.models.V1Deployment; +import io.kubernetes.client.openapi.models.V1Pod; +import io.kubernetes.client.util.ClientBuilder; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +/** Test for Yaml.createResource() functionality */ +class YamlCreateResourceTest { + + private static final String DISCOVERY_API = + new File( + YamlCreateResourceTest.class + .getClassLoader() + .getResource("discovery-api.json") + .getPath()) + .toString(); + + private static final String DISCOVERY_APIV1 = + new File( + YamlCreateResourceTest.class + .getClassLoader() + .getResource("discovery-api-v1.json") + .getPath()) + .toString(); + + private static final String DISCOVERY_APIS = + new File( + YamlCreateResourceTest.class + .getClassLoader() + .getResource("discovery-apis-with-apps.json") + .getPath()) + .toString(); + + private static final String DISCOVERY_APPS_V1 = + new File( + YamlCreateResourceTest.class + .getClassLoader() + .getResource("discovery-apps-v1.json") + .getPath()) + .toString(); + + private ApiClient apiClient; + + @RegisterExtension + static WireMockExtension apiServer = + WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build(); + + @BeforeEach + void setup() { + apiClient = new ClientBuilder().setBasePath("http://localhost:" + apiServer.getPort()).build(); + } + + @Test + void createConfigMapFromYaml() throws IOException, ApiException { + String configMapYaml = + "apiVersion: v1\n" + + "kind: ConfigMap\n" + + "metadata:\n" + + " name: test-config\n" + + " namespace: default\n" + + "data:\n" + + " key1: value1\n"; + + apiServer.stubFor( + post(urlPathEqualTo("/api/v1/namespaces/default/configmaps")) + .willReturn( + aResponse() + .withStatus(200) + .withBody( + "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\"," + + "\"metadata\":{\"name\":\"test-config\",\"namespace\":\"default\"}," + + "\"data\":{\"key1\":\"value1\"}}"))); + apiServer.stubFor( + get(urlPathEqualTo("/api")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API)))))); + apiServer.stubFor( + get(urlPathEqualTo("/apis")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS)))))); + apiServer.stubFor( + get(urlPathEqualTo("/api/v1")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1)))))); + + Object result = Yaml.createResource(apiClient, configMapYaml); + + apiServer.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/namespaces/default/configmaps"))); + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(V1ConfigMap.class); + + V1ConfigMap configMap = (V1ConfigMap) result; + assertThat(configMap.getMetadata().getName()).isEqualTo("test-config"); + assertThat(configMap.getMetadata().getNamespace()).isEqualTo("default"); + assertThat(configMap.getData()).containsEntry("key1", "value1"); + } + + @Test + void createPodFromYaml() throws IOException, ApiException { + String podYaml = + "apiVersion: v1\n" + + "kind: Pod\n" + + "metadata:\n" + + " name: test-pod\n" + + " namespace: default\n" + + "spec:\n" + + " containers:\n" + + " - name: nginx\n" + + " image: nginx:latest\n"; + + apiServer.stubFor( + post(urlPathEqualTo("/api/v1/namespaces/default/pods")) + .willReturn( + aResponse() + .withStatus(200) + .withBody( + "{\"apiVersion\":\"v1\",\"kind\":\"Pod\"," + + "\"metadata\":{\"name\":\"test-pod\",\"namespace\":\"default\"}," + + "\"spec\":{\"containers\":[{\"name\":\"nginx\",\"image\":\"nginx:latest\"}]}}"))); + apiServer.stubFor( + get(urlPathEqualTo("/api")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API)))))); + apiServer.stubFor( + get(urlPathEqualTo("/apis")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS)))))); + apiServer.stubFor( + get(urlPathEqualTo("/api/v1")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1)))))); + + Object result = Yaml.createResource(apiClient, podYaml); + + apiServer.verify(1, postRequestedFor(urlPathEqualTo("/api/v1/namespaces/default/pods"))); + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(V1Pod.class); + + V1Pod pod = (V1Pod) result; + assertThat(pod.getMetadata().getName()).isEqualTo("test-pod"); + assertThat(pod.getMetadata().getNamespace()).isEqualTo("default"); + } + + @Test + void createDeploymentFromYaml() throws IOException, ApiException { + String deploymentYaml = + "apiVersion: apps/v1\n" + + "kind: Deployment\n" + + "metadata:\n" + + " name: test-deployment\n" + + " namespace: default\n" + + "spec:\n" + + " replicas: 3\n" + + " selector:\n" + + " matchLabels:\n" + + " app: test\n" + + " template:\n" + + " metadata:\n" + + " labels:\n" + + " app: test\n" + + " spec:\n" + + " containers:\n" + + " - name: nginx\n" + + " image: nginx:latest\n"; + + apiServer.stubFor( + post(urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments")) + .willReturn( + aResponse() + .withStatus(200) + .withBody( + "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\"," + + "\"metadata\":{\"name\":\"test-deployment\",\"namespace\":\"default\"}," + + "\"spec\":{\"replicas\":3,\"selector\":{\"matchLabels\":{\"app\":\"test\"}}," + + "\"template\":{\"metadata\":{\"labels\":{\"app\":\"test\"}}," + + "\"spec\":{\"containers\":[{\"name\":\"nginx\",\"image\":\"nginx:latest\"}]}}}}"))); + apiServer.stubFor( + get(urlPathEqualTo("/api")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_API)))))); + apiServer.stubFor( + get(urlPathEqualTo("/apis")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIS)))))); + apiServer.stubFor( + get(urlPathEqualTo("/api/v1")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APIV1)))))); + apiServer.stubFor( + get(urlPathEqualTo("/apis/apps/v1")) + .willReturn( + aResponse() + .withStatus(200) + .withBody(new String(Files.readAllBytes(Paths.get(DISCOVERY_APPS_V1)))))); + + Object result = Yaml.createResource(apiClient, deploymentYaml); + + apiServer.verify( + 1, postRequestedFor(urlPathEqualTo("/apis/apps/v1/namespaces/default/deployments"))); + assertThat(result).isNotNull(); + assertThat(result).isInstanceOf(V1Deployment.class); + + V1Deployment deployment = (V1Deployment) result; + assertThat(deployment.getMetadata().getName()).isEqualTo("test-deployment"); + assertThat(deployment.getMetadata().getNamespace()).isEqualTo("default"); + } +} diff --git a/util/src/test/java/io/kubernetes/client/util/YamlTest.java b/util/src/test/java/io/kubernetes/client/util/YamlTest.java index 26f536a9a7..0a54c27851 100644 --- a/util/src/test/java/io/kubernetes/client/util/YamlTest.java +++ b/util/src/test/java/io/kubernetes/client/util/YamlTest.java @@ -18,6 +18,7 @@ import io.kubernetes.client.Resources; import io.kubernetes.client.common.KubernetesType; +import io.kubernetes.client.openapi.models.V1ConfigMap; import io.kubernetes.client.openapi.models.V1CustomResourceDefinition; import io.kubernetes.client.openapi.models.V1Deployment; import io.kubernetes.client.openapi.models.V1ObjectMeta; @@ -265,4 +266,34 @@ void loadDumpCRDWithIntOrStringExtension() { String dumped = Yaml.dump(crd); assertThat(dumped).isEqualTo(data); } + + @Test + void createResourceFromYaml() throws Exception { + // This test validates that the createResource method can parse YAML + // and determine the correct resource type without requiring the caller + // to specify the type upfront. + + String configMapYaml = + "apiVersion: v1\n" + + "kind: ConfigMap\n" + + "metadata:\n" + + " name: test-config\n" + + " namespace: default\n" + + "data:\n" + + " key1: value1\n"; + + // Note: This test only validates that the YAML can be parsed and the + // correct type is determined. It does not actually create the resource + // in a cluster, as that would require a real or mocked API server. + // The actual creation logic is tested in integration tests. + + // Test that we can load the YAML and determine the type + Object obj = Yaml.load(configMapYaml); + assertThat(obj).isInstanceOf(V1ConfigMap.class); + + V1ConfigMap configMap = (V1ConfigMap) obj; + assertThat(configMap.getMetadata().getName()).isEqualTo("test-config"); + assertThat(configMap.getMetadata().getNamespace()).isEqualTo("default"); + assertThat(configMap.getData()).containsEntry("key1", "value1"); + } } diff --git a/util/src/test/resources/discovery-api-v1.json b/util/src/test/resources/discovery-api-v1.json new file mode 100644 index 0000000000..5518eae2c1 --- /dev/null +++ b/util/src/test/resources/discovery-api-v1.json @@ -0,0 +1 @@ +{"kind":"APIResourceList","groupVersion":"v1","resources":[{"name":"bindings","singularName":"","namespaced":true,"kind":"Binding","verbs":["create"]},{"name":"componentstatuses","singularName":"","namespaced":false,"kind":"ComponentStatus","verbs":["get","list"],"shortNames":["cs"]},{"name":"configmaps","singularName":"","namespaced":true,"kind":"ConfigMap","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["cm"],"storageVersionHash":"qFsyl6wFWjQ="},{"name":"endpoints","singularName":"","namespaced":true,"kind":"Endpoints","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ep"],"storageVersionHash":"fWeeMqaN/OA="},{"name":"events","singularName":"","namespaced":true,"kind":"Event","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ev"],"storageVersionHash":"r2yiGXH7wu8="},{"name":"limitranges","singularName":"","namespaced":true,"kind":"LimitRange","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["limits"],"storageVersionHash":"EBKMFVe6cwo="},{"name":"namespaces","singularName":"","namespaced":false,"kind":"Namespace","verbs":["create","delete","get","list","patch","update","watch"],"shortNames":["ns"],"storageVersionHash":"Q3oi5N2YM8M="},{"name":"namespaces/finalize","singularName":"","namespaced":false,"kind":"Namespace","verbs":["update"]},{"name":"namespaces/status","singularName":"","namespaced":false,"kind":"Namespace","verbs":["get","patch","update"]},{"name":"nodes","singularName":"","namespaced":false,"kind":"Node","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["no"],"storageVersionHash":"XwShjMxG9Fs="},{"name":"nodes/proxy","singularName":"","namespaced":false,"kind":"NodeProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"nodes/status","singularName":"","namespaced":false,"kind":"Node","verbs":["get","patch","update"]},{"name":"persistentvolumeclaims","singularName":"","namespaced":true,"kind":"PersistentVolumeClaim","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["pvc"],"storageVersionHash":"QWTyNDq0dC4="},{"name":"persistentvolumeclaims/status","singularName":"","namespaced":true,"kind":"PersistentVolumeClaim","verbs":["get","patch","update"]},{"name":"persistentvolumes","singularName":"","namespaced":false,"kind":"PersistentVolume","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["pv"],"storageVersionHash":"HN/zwEC+JgM="},{"name":"persistentvolumes/status","singularName":"","namespaced":false,"kind":"PersistentVolume","verbs":["get","patch","update"]},{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["po"],"categories":["all"],"storageVersionHash":"xPOwRZ+Yhw8="},{"name":"pods/attach","singularName":"","namespaced":true,"kind":"PodAttachOptions","verbs":["create","get"]},{"name":"pods/binding","singularName":"","namespaced":true,"kind":"Binding","verbs":["create"]},{"name":"pods/eviction","singularName":"","namespaced":true,"group":"policy","version":"v1beta1","kind":"Eviction","verbs":["create"]},{"name":"pods/exec","singularName":"","namespaced":true,"kind":"PodExecOptions","verbs":["create","get"]},{"name":"pods/log","singularName":"","namespaced":true,"kind":"Pod","verbs":["get"]},{"name":"pods/portforward","singularName":"","namespaced":true,"kind":"PodPortForwardOptions","verbs":["create","get"]},{"name":"pods/proxy","singularName":"","namespaced":true,"kind":"PodProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"pods/status","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","patch","update"]},{"name":"podtemplates","singularName":"","namespaced":true,"kind":"PodTemplate","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"LIXB2x4IFpk="},{"name":"replicationcontrollers","singularName":"","namespaced":true,"kind":"ReplicationController","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["rc"],"categories":["all"],"storageVersionHash":"Jond2If31h0="},{"name":"replicationcontrollers/scale","singularName":"","namespaced":true,"group":"autoscaling","version":"v1","kind":"Scale","verbs":["get","patch","update"]},{"name":"replicationcontrollers/status","singularName":"","namespaced":true,"kind":"ReplicationController","verbs":["get","patch","update"]},{"name":"resourcequotas","singularName":"","namespaced":true,"kind":"ResourceQuota","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["quota"],"storageVersionHash":"8uhSgffRX6w="},{"name":"resourcequotas/status","singularName":"","namespaced":true,"kind":"ResourceQuota","verbs":["get","patch","update"]},{"name":"secrets","singularName":"","namespaced":true,"kind":"Secret","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"S6u1pOWzb84="},{"name":"serviceaccounts","singularName":"","namespaced":true,"kind":"ServiceAccount","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["sa"],"storageVersionHash":"pbx9ZvyFpBE="},{"name":"services","singularName":"","namespaced":true,"kind":"Service","verbs":["create","delete","get","list","patch","update","watch"],"shortNames":["svc"],"categories":["all"],"storageVersionHash":"0/CO1lhkEBI="},{"name":"services/proxy","singularName":"","namespaced":true,"kind":"ServiceProxyOptions","verbs":["create","delete","get","patch","update"]},{"name":"services/status","singularName":"","namespaced":true,"kind":"Service","verbs":["get","patch","update"]}]} \ No newline at end of file diff --git a/util/src/test/resources/discovery-api.json b/util/src/test/resources/discovery-api.json new file mode 100644 index 0000000000..15ae6ec90e --- /dev/null +++ b/util/src/test/resources/discovery-api.json @@ -0,0 +1 @@ +{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0","serverAddress":"192.168.65.3:6443"}]} \ No newline at end of file diff --git a/util/src/test/resources/discovery-apis-with-apps.json b/util/src/test/resources/discovery-apis-with-apps.json new file mode 100644 index 0000000000..de01c17579 --- /dev/null +++ b/util/src/test/resources/discovery-apis-with-apps.json @@ -0,0 +1 @@ +{"kind":"APIGroupList","apiVersion":"v1","groups":[{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}}]} diff --git a/util/src/test/resources/discovery-apis.json b/util/src/test/resources/discovery-apis.json new file mode 100644 index 0000000000..de832c919e --- /dev/null +++ b/util/src/test/resources/discovery-apis.json @@ -0,0 +1 @@ +{"kind":"APIGroupList","apiVersion":"v1","groups":[]} \ No newline at end of file diff --git a/util/src/test/resources/discovery-apps-v1.json b/util/src/test/resources/discovery-apps-v1.json new file mode 100644 index 0000000000..236778e886 --- /dev/null +++ b/util/src/test/resources/discovery-apps-v1.json @@ -0,0 +1 @@ +{"kind":"APIResourceList","groupVersion":"apps/v1","resources":[{"name":"controllerrevisions","singularName":"","namespaced":true,"kind":"ControllerRevision","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"storageVersionHash":"85nkx63pcBU="},{"name":"daemonsets","singularName":"","namespaced":true,"kind":"DaemonSet","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["ds"],"categories":["all"],"storageVersionHash":"dd7pWHUlMKQ="},{"name":"daemonsets/status","singularName":"","namespaced":true,"kind":"DaemonSet","verbs":["get","patch","update"]},{"name":"deployments","singularName":"","namespaced":true,"kind":"Deployment","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["deploy"],"categories":["all"],"storageVersionHash":"8aSe+NMegvE="},{"name":"deployments/scale","singularName":"","namespaced":true,"group":"autoscaling","version":"v1","kind":"Scale","verbs":["get","patch","update"]},{"name":"deployments/status","singularName":"","namespaced":true,"kind":"Deployment","verbs":["get","patch","update"]},{"name":"replicasets","singularName":"","namespaced":true,"kind":"ReplicaSet","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["rs"],"categories":["all"],"storageVersionHash":"P1RzHs8/mWQ="},{"name":"replicasets/scale","singularName":"","namespaced":true,"group":"autoscaling","version":"v1","kind":"Scale","verbs":["get","patch","update"]},{"name":"replicasets/status","singularName":"","namespaced":true,"kind":"ReplicaSet","verbs":["get","patch","update"]},{"name":"statefulsets","singularName":"","namespaced":true,"kind":"StatefulSet","verbs":["create","delete","deletecollection","get","list","patch","update","watch"],"shortNames":["sts"],"categories":["all"],"storageVersionHash":"H+vl74LkKdo="},{"name":"statefulsets/scale","singularName":"","namespaced":true,"group":"autoscaling","version":"v1","kind":"Scale","verbs":["get","patch","update"]},{"name":"statefulsets/status","singularName":"","namespaced":true,"kind":"StatefulSet","verbs":["get","patch","update"]}]}