|
39 | 39 | import org.slf4j.Logger; |
40 | 40 | import org.slf4j.LoggerFactory; |
41 | 41 | import org.yaml.snakeyaml.DumperOptions; |
42 | | -import org.yaml.snakeyaml.DumperOptions.FlowStyle; |
43 | 42 | import org.yaml.snakeyaml.LoaderOptions; |
44 | 43 | import org.yaml.snakeyaml.TypeDescription; |
45 | 44 | import org.yaml.snakeyaml.constructor.BaseConstructor; |
@@ -367,7 +366,7 @@ protected NodeTuple representJavaBeanProperty( |
367 | 366 | if (propertyValue == null) { |
368 | 367 | return null; |
369 | 368 | } |
370 | | - if (propertyValue instanceof List<?> && ((List<?>)propertyValue).size() == 0) { |
| 369 | + if (propertyValue instanceof List<?> && ((List<?>) propertyValue).size() == 0) { |
371 | 370 | return null; |
372 | 371 | } |
373 | 372 | return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); |
@@ -566,4 +565,174 @@ private static Object modelMapper(Map<String, Object> data) throws IOException { |
566 | 565 | public static void addModelMap(String apiGroupVersion, String kind, Class<?> clazz) { |
567 | 566 | ModelMapper.addModelMap(apiGroupVersion, kind, clazz); |
568 | 567 | } |
| 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 | + } |
569 | 738 | } |
0 commit comments