Skip to content

Commit 32e651a

Browse files
authored
Merge pull request #4427 from kubernetes-client/copilot/extend-kubectl-create-yaml
Add Yaml.createResource() for type-agnostic resource creation from YAML
2 parents 98fd601 + d11e516 commit 32e651a

File tree

9 files changed

+573
-11
lines changed

9 files changed

+573
-11
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
package io.kubernetes.client.examples;
14+
15+
import io.kubernetes.client.openapi.ApiClient;
16+
import io.kubernetes.client.openapi.ApiException;
17+
import io.kubernetes.client.openapi.Configuration;
18+
import io.kubernetes.client.openapi.apis.CoreV1Api;
19+
import io.kubernetes.client.openapi.models.V1ConfigMap;
20+
import io.kubernetes.client.openapi.models.V1Pod;
21+
import io.kubernetes.client.util.Config;
22+
import io.kubernetes.client.util.Yaml;
23+
import java.io.File;
24+
import java.io.IOException;
25+
26+
/**
27+
* A simple example of how to use Yaml.createResource() to create Kubernetes resources from YAML
28+
* without specifying the type upfront. This is equivalent to `kubectl create -f <yaml-file>`.
29+
*
30+
* <p>Easiest way to run this: mvn exec:java
31+
* -Dexec.mainClass="io.kubernetes.client.examples.YamlCreateResourceExample"
32+
*
33+
* <p>From inside $REPO_DIR/examples
34+
*/
35+
public class YamlCreateResourceExample {
36+
public static void main(String[] args) throws IOException, ApiException {
37+
// Initialize the API client
38+
ApiClient client = Config.defaultClient();
39+
Configuration.setDefaultApiClient(client);
40+
41+
// Example 1: Create a ConfigMap from YAML string
42+
// This method automatically determines the resource type (ConfigMap)
43+
// and uses the appropriate API to create it
44+
String configMapYaml =
45+
"apiVersion: v1\n"
46+
+ "kind: ConfigMap\n"
47+
+ "metadata:\n"
48+
+ " name: example-config\n"
49+
+ " namespace: default\n"
50+
+ "data:\n"
51+
+ " database.url: jdbc:postgresql://localhost/mydb\n"
52+
+ " database.user: admin\n";
53+
54+
System.out.println("Creating ConfigMap from YAML string...");
55+
Object configMapResult = Yaml.createResource(client, configMapYaml);
56+
System.out.println("Created: " + configMapResult);
57+
58+
// Example 2: Create a Pod from YAML string
59+
// Again, no need to specify V1Pod.class - the method determines it automatically
60+
String podYaml =
61+
"apiVersion: v1\n"
62+
+ "kind: Pod\n"
63+
+ "metadata:\n"
64+
+ " name: example-pod\n"
65+
+ " namespace: default\n"
66+
+ "spec:\n"
67+
+ " containers:\n"
68+
+ " - name: nginx\n"
69+
+ " image: nginx:1.14.2\n"
70+
+ " ports:\n"
71+
+ " - containerPort: 80\n";
72+
73+
System.out.println("\nCreating Pod from YAML string...");
74+
Object podResult = Yaml.createResource(client, podYaml);
75+
System.out.println("Created: " + podResult);
76+
77+
// Example 3: Create a resource from a YAML file
78+
// This works with any Kubernetes resource type
79+
File yamlFile = new File("example-resource.yaml");
80+
if (yamlFile.exists()) {
81+
System.out.println("\nCreating resource from YAML file...");
82+
Object fileResult = Yaml.createResource(client, yamlFile);
83+
System.out.println("Created: " + fileResult);
84+
}
85+
86+
// Example 4: Type casting if you need to access specific fields
87+
// The returned object is the strongly-typed Kubernetes object
88+
V1ConfigMap configMap = (V1ConfigMap) configMapResult;
89+
System.out.println("\nConfigMap name: " + configMap.getMetadata().getName());
90+
System.out.println("ConfigMap data: " + configMap.getData());
91+
92+
V1Pod pod = (V1Pod) podResult;
93+
System.out.println("\nPod name: " + pod.getMetadata().getName());
94+
System.out.println("Pod phase: " + pod.getStatus().getPhase());
95+
96+
// Clean up - delete the created resources
97+
CoreV1Api api = new CoreV1Api();
98+
System.out.println("\nCleaning up...");
99+
api.deleteNamespacedConfigMap("example-config", "default").execute();
100+
System.out.println("Deleted ConfigMap");
101+
102+
api.deleteNamespacedPod("example-pod", "default").execute();
103+
System.out.println("Deleted Pod");
104+
}
105+
}

util/src/main/java/io/kubernetes/client/util/Yaml.java

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.slf4j.Logger;
4040
import org.slf4j.LoggerFactory;
4141
import org.yaml.snakeyaml.DumperOptions;
42-
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
4342
import org.yaml.snakeyaml.LoaderOptions;
4443
import org.yaml.snakeyaml.TypeDescription;
4544
import org.yaml.snakeyaml.constructor.BaseConstructor;
@@ -367,7 +366,7 @@ protected NodeTuple representJavaBeanProperty(
367366
if (propertyValue == null) {
368367
return null;
369368
}
370-
if (propertyValue instanceof List<?> && ((List<?>)propertyValue).size() == 0) {
369+
if (propertyValue instanceof List<?> && ((List<?>) propertyValue).size() == 0) {
371370
return null;
372371
}
373372
return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
@@ -566,4 +565,174 @@ private static Object modelMapper(Map<String, Object> data) throws IOException {
566565
public static void addModelMap(String apiGroupVersion, String kind, Class<?> clazz) {
567566
ModelMapper.addModelMap(apiGroupVersion, kind, clazz);
568567
}
568+
569+
/**
570+
* Create a Kubernetes resource from a YAML string. This method automatically determines the
571+
* resource type from the YAML content (apiVersion and kind) and uses the appropriate API to
572+
* create the resource.
573+
*
574+
* <p>This is equivalent to `kubectl create -f <yaml-content>`.
575+
*
576+
* <p>Example usage:
577+
*
578+
* <pre>{@code
579+
* ApiClient client = Config.defaultClient();
580+
* String yaml = "apiVersion: v1\n" +
581+
* "kind: ConfigMap\n" +
582+
* "metadata:\n" +
583+
* " name: my-config\n" +
584+
* " namespace: default\n";
585+
* Object created = Yaml.createResource(client, yaml);
586+
* }</pre>
587+
*
588+
* @param client The API client to use for creating the resource
589+
* @param content The YAML content as a string
590+
* @return The created resource object
591+
* @throws IOException If an error occurs while reading or parsing the YAML
592+
* @throws io.kubernetes.client.openapi.ApiException If an error occurs while creating the
593+
* resource in the cluster
594+
*/
595+
public static Object createResource(io.kubernetes.client.openapi.ApiClient client, String content)
596+
throws IOException, io.kubernetes.client.openapi.ApiException {
597+
return createResource(client, new StringReader(content));
598+
}
599+
600+
/**
601+
* Create a Kubernetes resource from a YAML file. This method automatically determines the
602+
* resource type from the YAML content (apiVersion and kind) and uses the appropriate API to
603+
* create the resource.
604+
*
605+
* <p>This is equivalent to `kubectl create -f <yaml-file>`.
606+
*
607+
* @param client The API client to use for creating the resource
608+
* @param f The YAML file to load
609+
* @return The created resource object
610+
* @throws IOException If an error occurs while reading or parsing the YAML
611+
* @throws io.kubernetes.client.openapi.ApiException If an error occurs while creating the
612+
* resource in the cluster
613+
*/
614+
public static Object createResource(io.kubernetes.client.openapi.ApiClient client, File f)
615+
throws IOException, io.kubernetes.client.openapi.ApiException {
616+
return createResource(client, new FileReader(f));
617+
}
618+
619+
/**
620+
* Create a Kubernetes resource from a YAML stream. This method automatically determines the
621+
* resource type from the YAML content (apiVersion and kind) and uses the appropriate API to
622+
* create the resource.
623+
*
624+
* <p>This is equivalent to `kubectl create -f <yaml-stream>`.
625+
*
626+
* @param client The API client to use for creating the resource
627+
* @param reader The stream to load
628+
* @return The created resource object
629+
* @throws IOException If an error occurs while reading or parsing the YAML
630+
* @throws io.kubernetes.client.openapi.ApiException If an error occurs while creating the
631+
* resource in the cluster
632+
*/
633+
public static Object createResource(io.kubernetes.client.openapi.ApiClient client, Reader reader)
634+
throws IOException, io.kubernetes.client.openapi.ApiException {
635+
// Read the entire content into a string first so we can parse it twice
636+
StringBuilder sb = new StringBuilder();
637+
char[] buffer = new char[8192];
638+
int read;
639+
while ((read = reader.read(buffer)) != -1) {
640+
sb.append(buffer, 0, read);
641+
}
642+
String yamlContent = sb.toString();
643+
644+
// Load the YAML as a map to extract apiVersion and kind
645+
// Note: The getSnakeYaml() method already configures LoaderOptions with appropriate
646+
// security settings to prevent YAML bombs and other attacks
647+
Map<String, Object> data = getSnakeYaml(null).load(new StringReader(yamlContent));
648+
649+
String kind = (String) data.get("kind");
650+
if (kind == null) {
651+
throw new IOException("Missing kind in YAML!");
652+
}
653+
String apiVersion = (String) data.get("apiVersion");
654+
if (apiVersion == null) {
655+
throw new IOException("Missing apiVersion in YAML!");
656+
}
657+
658+
// Use ModelMapper to get the appropriate class for this resource type
659+
Class<?> clazz = ModelMapper.getApiTypeClass(apiVersion, kind);
660+
if (clazz == null) {
661+
throw new IOException(
662+
"Unknown apiVersion/kind: " + apiVersion + "/" + kind + ". Is it registered?");
663+
}
664+
665+
// Load the YAML into the strongly typed object using the same content
666+
Object resource = loadAs(new StringReader(yamlContent), clazz);
667+
668+
// Ensure the resource is a KubernetesObject
669+
if (!(resource instanceof io.kubernetes.client.common.KubernetesObject)) {
670+
throw new IOException("Resource is not a KubernetesObject: " + resource.getClass().getName());
671+
}
672+
673+
io.kubernetes.client.common.KubernetesObject k8sObject =
674+
(io.kubernetes.client.common.KubernetesObject) resource;
675+
676+
// Parse apiVersion to extract group and version
677+
io.kubernetes.client.apimachinery.GroupVersionKind gvk =
678+
ModelMapper.groupVersionKindFromApiVersionAndKind(apiVersion, kind);
679+
680+
// Get the resource metadata to determine the plural name
681+
io.kubernetes.client.apimachinery.GroupVersionResource gvr =
682+
ModelMapper.getGroupVersionResourceByClass(clazz);
683+
684+
if (gvr == null) {
685+
// If no GVR mapping exists, we need to perform discovery
686+
io.kubernetes.client.Discovery discovery = new io.kubernetes.client.Discovery(client);
687+
ModelMapper.refresh(discovery);
688+
gvr = ModelMapper.getGroupVersionResourceByClass(clazz);
689+
690+
if (gvr == null) {
691+
throw new IOException(
692+
"Unable to determine resource plural name for " + apiVersion + "/" + kind);
693+
}
694+
}
695+
696+
// Create a GenericKubernetesApi for this resource type
697+
io.kubernetes.client.util.generic.GenericKubernetesApi<
698+
io.kubernetes.client.common.KubernetesObject,
699+
io.kubernetes.client.common.KubernetesListObject>
700+
api =
701+
new io.kubernetes.client.util.generic.GenericKubernetesApi<>(
702+
(Class<io.kubernetes.client.common.KubernetesObject>) clazz,
703+
io.kubernetes.client.common.KubernetesListObject.class,
704+
gvk.getGroup(),
705+
gvk.getVersion(),
706+
gvr.getResource(),
707+
client);
708+
709+
// Create the resource
710+
io.kubernetes.client.util.generic.KubernetesApiResponse<
711+
io.kubernetes.client.common.KubernetesObject>
712+
response;
713+
714+
Boolean isNamespaced = ModelMapper.isNamespaced(clazz);
715+
if (isNamespaced != null && isNamespaced) {
716+
// For namespaced resources
717+
String namespace = k8sObject.getMetadata().getNamespace();
718+
if (namespace == null || namespace.isEmpty()) {
719+
// Default to "default" namespace, matching kubectl behavior
720+
namespace = "default";
721+
}
722+
response =
723+
api.create(
724+
namespace, k8sObject, new io.kubernetes.client.util.generic.options.CreateOptions());
725+
} else {
726+
// For cluster-scoped resources
727+
response =
728+
api.create(k8sObject, new io.kubernetes.client.util.generic.options.CreateOptions());
729+
}
730+
731+
if (!response.isSuccess()) {
732+
throw new io.kubernetes.client.openapi.ApiException(
733+
response.getHttpStatusCode(), "Failed to create resource: " + response.getStatus());
734+
}
735+
736+
return response.getObject();
737+
}
569738
}

0 commit comments

Comments
 (0)