diff --git a/util/src/main/java/io/kubernetes/client/util/ResourceList.java b/util/src/main/java/io/kubernetes/client/util/ResourceList.java
new file mode 100644
index 0000000000..791c36156c
--- /dev/null
+++ b/util/src/main/java/io/kubernetes/client/util/ResourceList.java
@@ -0,0 +1,648 @@
+/*
+Copyright 2024 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 io.kubernetes.client.common.KubernetesObject;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import io.kubernetes.client.util.generic.KubernetesApiResponse;
+import io.kubernetes.client.util.generic.options.DeleteOptions;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Batch operations for multiple Kubernetes resources.
+ * Provides fabric8-style resourceList() operations for creating, deleting,
+ * and managing multiple resources at once.
+ *
+ *
Example usage:
+ *
{@code
+ * // Load and create multiple resources from a file
+ * List
+ */
+public class ResourceList {
+
+ private final ApiClient apiClient;
+ private final List resources;
+ private String namespace;
+ private boolean continueOnError = false;
+
+ private ResourceList(ApiClient apiClient, List resources) {
+ this.apiClient = Objects.requireNonNull(apiClient, "ApiClient must not be null");
+ this.resources = new ArrayList<>(Objects.requireNonNull(resources, "Resources must not be null"));
+ }
+
+ /**
+ * Create a ResourceList from a list of resources.
+ *
+ * @param apiClient the API client
+ * @param resources the resources
+ * @return a new ResourceList
+ */
+ public static ResourceList from(ApiClient apiClient, List resources) {
+ return new ResourceList(apiClient, resources);
+ }
+
+ /**
+ * Create a ResourceList from resources.
+ *
+ * @param apiClient the API client
+ * @param resources the resources
+ * @return a new ResourceList
+ */
+ public static ResourceList from(ApiClient apiClient, Object... resources) {
+ return new ResourceList(apiClient, Arrays.asList(resources));
+ }
+
+ /**
+ * Load resources from a YAML file.
+ *
+ * @param apiClient the API client
+ * @param file the YAML file
+ * @return a new ResourceList
+ * @throws IOException if an error occurs reading the file
+ */
+ public static ResourceList fromFile(ApiClient apiClient, File file) throws IOException {
+ List resources = ResourceLoader.loadAll(file);
+ return new ResourceList(apiClient, resources);
+ }
+
+ /**
+ * Load resources from an InputStream.
+ *
+ * @param apiClient the API client
+ * @param inputStream the input stream
+ * @return a new ResourceList
+ * @throws IOException if an error occurs reading the stream
+ */
+ public static ResourceList fromStream(ApiClient apiClient, InputStream inputStream) throws IOException {
+ List resources = ResourceLoader.loadAll(inputStream);
+ return new ResourceList(apiClient, resources);
+ }
+
+ /**
+ * Load resources from a YAML string.
+ *
+ * @param apiClient the API client
+ * @param yaml the YAML content
+ * @return a new ResourceList
+ * @throws IOException if an error occurs parsing the YAML
+ */
+ public static ResourceList fromYaml(ApiClient apiClient, String yaml) throws IOException {
+ List resources = ResourceLoader.loadAll(yaml);
+ return new ResourceList(apiClient, resources);
+ }
+
+ /**
+ * Get the list of resources.
+ *
+ * @return the resources
+ */
+ public List getResources() {
+ return Collections.unmodifiableList(resources);
+ }
+
+ /**
+ * Specify the namespace for all resources.
+ * This will override the namespace specified in individual resources.
+ *
+ * @param namespace the namespace
+ * @return this ResourceList for chaining
+ */
+ public ResourceList inNamespace(String namespace) {
+ this.namespace = namespace;
+ return this;
+ }
+
+ /**
+ * Continue processing remaining resources even if one fails.
+ *
+ * @param continueOnError whether to continue on error
+ * @return this ResourceList for chaining
+ */
+ public ResourceList continueOnError(boolean continueOnError) {
+ this.continueOnError = continueOnError;
+ return this;
+ }
+
+ /**
+ * Create all resources in the cluster.
+ *
+ * @return list of created resources
+ * @throws ApiException if an API error occurs
+ */
+ public List create() throws ApiException {
+ List created = new ArrayList<>();
+ List errors = new ArrayList<>();
+
+ for (Object resource : resources) {
+ try {
+ Object result = createResource(resource);
+ created.add(result);
+ } catch (ApiException e) {
+ if (continueOnError) {
+ errors.add(e);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ throw new BatchOperationException("Some resources failed to create", errors, created);
+ }
+ return created;
+ }
+
+ /**
+ * Create or replace all resources in the cluster.
+ *
+ * @return list of created or replaced resources
+ * @throws ApiException if an API error occurs
+ */
+ public List createOrReplace() throws ApiException {
+ List results = new ArrayList<>();
+ List errors = new ArrayList<>();
+
+ for (Object resource : resources) {
+ try {
+ Object result = createOrReplaceResource(resource);
+ results.add(result);
+ } catch (ApiException e) {
+ if (continueOnError) {
+ errors.add(e);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ throw new BatchOperationException("Some resources failed to create/replace", errors, results);
+ }
+ return results;
+ }
+
+ /**
+ * Delete all resources from the cluster.
+ *
+ * @throws ApiException if an API error occurs
+ */
+ public void delete() throws ApiException {
+ List errors = new ArrayList<>();
+
+ // Delete in reverse order (dependency order)
+ for (int i = resources.size() - 1; i >= 0; i--) {
+ Object resource = resources.get(i);
+ try {
+ deleteResource(resource);
+ } catch (ApiException e) {
+ // Ignore 404 - resource already deleted
+ if (e.getCode() != 404) {
+ if (continueOnError) {
+ errors.add(e);
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ throw new BatchOperationException("Some resources failed to delete", errors, Collections.emptyList());
+ }
+ }
+
+ /**
+ * Apply all resources using server-side apply.
+ *
+ * @param fieldManager the field manager name
+ * @return list of applied resources
+ * @throws ApiException if an API error occurs
+ */
+ public List serverSideApply(String fieldManager) throws ApiException {
+ return serverSideApply(fieldManager, false);
+ }
+
+ /**
+ * Apply all resources using server-side apply.
+ *
+ * @param fieldManager the field manager name
+ * @param forceConflicts whether to force ownership of conflicting fields
+ * @return list of applied resources
+ * @throws ApiException if an API error occurs
+ */
+ public List serverSideApply(String fieldManager, boolean forceConflicts) throws ApiException {
+ List applied = new ArrayList<>();
+ List errors = new ArrayList<>();
+
+ for (Object resource : resources) {
+ try {
+ Object result = serverSideApplyResource(resource, fieldManager, forceConflicts);
+ applied.add(result);
+ } catch (ApiException e) {
+ if (continueOnError) {
+ errors.add(e);
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ throw new BatchOperationException("Some resources failed to apply", errors, applied);
+ }
+ return applied;
+ }
+
+ /**
+ * Wait until all resources are ready.
+ *
+ * @param timeout the maximum time to wait for each resource
+ * @throws ApiException if an API error occurs
+ * @throws InterruptedException if the wait is interrupted
+ * @throws TimeoutException if the timeout is exceeded
+ */
+ public void waitUntilAllReady(Duration timeout)
+ throws ApiException, InterruptedException, TimeoutException {
+ for (Object resource : resources) {
+ if (resource instanceof KubernetesObject) {
+ waitForResource((KubernetesObject) resource, timeout);
+ }
+ }
+ }
+
+ /**
+ * Wait until all resources are ready, returning a CompletableFuture.
+ *
+ * @param timeout the maximum time to wait for each resource
+ * @return a CompletableFuture that completes when all resources are ready
+ */
+ public CompletableFuture waitUntilAllReadyAsync(Duration timeout) {
+ List> futures = resources.stream()
+ .filter(r -> r instanceof KubernetesObject)
+ .map(r -> waitForResourceAsync((KubernetesObject) r, timeout))
+ .collect(Collectors.toList());
+
+ return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
+ }
+
+ /**
+ * Wait until all resources are deleted.
+ *
+ * @param timeout the maximum time to wait for each resource
+ * @throws ApiException if an API error occurs
+ * @throws InterruptedException if the wait is interrupted
+ * @throws TimeoutException if the timeout is exceeded
+ */
+ public void waitUntilAllDeleted(Duration timeout)
+ throws ApiException, InterruptedException, TimeoutException {
+ for (Object resource : resources) {
+ if (resource instanceof KubernetesObject) {
+ waitForDeletion((KubernetesObject) resource, timeout);
+ }
+ }
+ }
+
+ /**
+ * Check if all resources are ready.
+ *
+ * @return true if all resources are ready
+ * @throws ApiException if an API error occurs
+ */
+ public boolean areAllReady() throws ApiException {
+ for (Object resource : resources) {
+ if (resource instanceof KubernetesObject) {
+ KubernetesObject k8sObj = (KubernetesObject) resource;
+ DynamicKubernetesObject dynamicObj = toDynamicObject(k8sObj);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ String ns = getEffectiveNamespace(dynamicObj);
+ String name = dynamicObj.getMetadata().getName();
+
+ KubernetesApiResponse response;
+ if (ns != null) {
+ response = dynamicApi.get(ns, name);
+ } else {
+ response = dynamicApi.get(name);
+ }
+
+ if (!response.isSuccess()) {
+ return false;
+ }
+
+ if (!Readiness.isReady(response.getObject())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ // Internal methods
+
+ private Object createResource(Object resource) throws ApiException {
+ if (resource instanceof KubernetesObject) {
+ KubernetesObject k8sObj = (KubernetesObject) resource;
+ DynamicKubernetesObject dynamicObj = toDynamicObject(k8sObj);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ String ns = getEffectiveNamespace(dynamicObj);
+ io.kubernetes.client.util.generic.options.CreateOptions createOpts =
+ new io.kubernetes.client.util.generic.options.CreateOptions();
+
+ KubernetesApiResponse response;
+ if (ns != null) {
+ response = dynamicApi.create(ns, dynamicObj, createOpts);
+ } else {
+ response = dynamicApi.create(dynamicObj, createOpts);
+ }
+
+ return response.throwsApiException().getObject();
+ }
+ throw new IllegalArgumentException("Resource must be a KubernetesObject");
+ }
+
+ private Object createOrReplaceResource(Object resource) throws ApiException {
+ if (resource instanceof KubernetesObject) {
+ KubernetesObject k8sObj = (KubernetesObject) resource;
+ DynamicKubernetesObject dynamicObj = toDynamicObject(k8sObj);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ String ns = getEffectiveNamespace(dynamicObj);
+ String name = dynamicObj.getMetadata().getName();
+ io.kubernetes.client.util.generic.options.CreateOptions createOpts =
+ new io.kubernetes.client.util.generic.options.CreateOptions();
+
+ // Try to get existing
+ KubernetesApiResponse existing;
+ if (ns != null) {
+ existing = dynamicApi.get(ns, name);
+ } else {
+ existing = dynamicApi.get(name);
+ }
+
+ KubernetesApiResponse response;
+ if (existing.isSuccess()) {
+ // Update existing
+ dynamicObj.getMetadata().setResourceVersion(
+ existing.getObject().getMetadata().getResourceVersion());
+ response = dynamicApi.update(dynamicObj);
+ } else {
+ // Create new
+ if (ns != null) {
+ response = dynamicApi.create(ns, dynamicObj, createOpts);
+ } else {
+ response = dynamicApi.create(dynamicObj, createOpts);
+ }
+ }
+
+ return response.throwsApiException().getObject();
+ }
+ throw new IllegalArgumentException("Resource must be a KubernetesObject");
+ }
+
+ private void deleteResource(Object resource) throws ApiException {
+ if (resource instanceof KubernetesObject) {
+ KubernetesObject k8sObj = (KubernetesObject) resource;
+ DynamicKubernetesObject dynamicObj = toDynamicObject(k8sObj);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ String ns = getEffectiveNamespace(dynamicObj);
+ String name = dynamicObj.getMetadata().getName();
+
+ KubernetesApiResponse response;
+ if (ns != null) {
+ response = dynamicApi.delete(ns, name);
+ } else {
+ response = dynamicApi.delete(name);
+ }
+
+ response.throwsApiException();
+ }
+ }
+
+ private Object serverSideApplyResource(Object resource, String fieldManager, boolean forceConflicts)
+ throws ApiException {
+ if (resource instanceof KubernetesObject) {
+ KubernetesObject k8sObj = (KubernetesObject) resource;
+ DynamicKubernetesObject dynamicObj = toDynamicObject(k8sObj);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ // Override namespace if specified
+ if (namespace != null) {
+ dynamicObj.getMetadata().setNamespace(namespace);
+ }
+
+ String ns = dynamicObj.getMetadata().getNamespace();
+ String name = dynamicObj.getMetadata().getName();
+
+ io.kubernetes.client.util.generic.options.PatchOptions patchOptions =
+ new io.kubernetes.client.util.generic.options.PatchOptions();
+ patchOptions.setFieldManager(fieldManager);
+ patchOptions.setForce(forceConflicts);
+
+ String yaml = Yaml.dump(k8sObj);
+ io.kubernetes.client.custom.V1Patch patch = new io.kubernetes.client.custom.V1Patch(yaml);
+
+ KubernetesApiResponse response;
+ if (ns != null) {
+ response = dynamicApi.patch(ns, name,
+ io.kubernetes.client.custom.V1Patch.PATCH_FORMAT_APPLY_YAML, patch, patchOptions);
+ } else {
+ response = dynamicApi.patch(name,
+ io.kubernetes.client.custom.V1Patch.PATCH_FORMAT_APPLY_YAML, patch, patchOptions);
+ }
+
+ return response.throwsApiException().getObject();
+ }
+ throw new IllegalArgumentException("Resource must be a KubernetesObject");
+ }
+
+ private void waitForResource(KubernetesObject resource, Duration timeout)
+ throws ApiException, InterruptedException, TimeoutException {
+ DynamicKubernetesObject dynamicObj = toDynamicObject(resource);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ String ns = getEffectiveNamespace(dynamicObj);
+ String name = dynamicObj.getMetadata().getName();
+
+ WaitUtils.waitUntilCondition(
+ () -> {
+ KubernetesApiResponse response = ns != null
+ ? dynamicApi.get(ns, name)
+ : dynamicApi.get(name);
+ return response.isSuccess() ? response.getObject() : null;
+ },
+ obj -> Readiness.isReady(obj),
+ timeout);
+ }
+
+ private CompletableFuture waitForResourceAsync(KubernetesObject resource, Duration timeout) {
+ return CompletableFuture.runAsync(() -> {
+ try {
+ waitForResource(resource, timeout);
+ } catch (ApiException | InterruptedException | TimeoutException e) {
+ throw new CompletionException(e);
+ }
+ });
+ }
+
+ private void waitForDeletion(KubernetesObject resource, Duration timeout)
+ throws ApiException, InterruptedException, TimeoutException {
+ DynamicKubernetesObject dynamicObj = toDynamicObject(resource);
+ DynamicKubernetesApi dynamicApi = getDynamicApi(dynamicObj);
+
+ String ns = getEffectiveNamespace(dynamicObj);
+ String name = dynamicObj.getMetadata().getName();
+
+ WaitUtils.waitUntilDeleted(
+ () -> {
+ KubernetesApiResponse response = ns != null
+ ? dynamicApi.get(ns, name)
+ : dynamicApi.get(name);
+ return response.isSuccess() ? response.getObject() : null;
+ },
+ timeout);
+ }
+
+ private DynamicKubernetesObject toDynamicObject(KubernetesObject obj) {
+ try {
+ String yaml = Yaml.dump(obj);
+ return Yaml.loadAs(yaml, DynamicKubernetesObject.class);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to convert to dynamic object", e);
+ }
+ }
+
+ private DynamicKubernetesApi getDynamicApi(DynamicKubernetesObject obj) {
+ String apiVersion = obj.getApiVersion();
+ String kind = obj.getKind();
+
+ String group;
+ String version;
+ if (apiVersion.contains("/")) {
+ String[] parts = apiVersion.split("/");
+ group = parts[0];
+ version = parts[1];
+ } else {
+ group = "";
+ version = apiVersion;
+ }
+
+ String plural = pluralize(kind);
+
+ return new DynamicKubernetesApi(group, version, plural, apiClient);
+ }
+
+ /**
+ * Simple pluralization for Kubernetes resource kinds.
+ */
+ private String pluralize(String kind) {
+ if (kind == null) {
+ return null;
+ }
+ String lower = kind.toLowerCase();
+ // Special cases for Kubernetes kinds
+ if (lower.endsWith("s") || lower.endsWith("x") || lower.endsWith("z")
+ || lower.endsWith("ch") || lower.endsWith("sh")) {
+ return lower + "es";
+ }
+ if (lower.endsWith("y") && lower.length() > 1) {
+ char beforeY = lower.charAt(lower.length() - 2);
+ if (beforeY != 'a' && beforeY != 'e' && beforeY != 'i' && beforeY != 'o' && beforeY != 'u') {
+ return lower.substring(0, lower.length() - 1) + "ies";
+ }
+ }
+ // Handle known Kubernetes kinds
+ switch (lower) {
+ case "endpoints":
+ return "endpoints";
+ case "ingress":
+ return "ingresses";
+ default:
+ return lower + "s";
+ }
+ }
+
+ private String getEffectiveNamespace(DynamicKubernetesObject obj) {
+ if (namespace != null) {
+ return namespace;
+ }
+ return obj.getMetadata().getNamespace();
+ }
+
+ /**
+ * Exception thrown when a batch operation has partial failures.
+ */
+ public static class BatchOperationException extends ApiException {
+ private final List failures;
+ private final List successfulResults;
+
+ public BatchOperationException(String message, List failures, List successfulResults) {
+ super(message);
+ this.failures = Collections.unmodifiableList(failures);
+ this.successfulResults = Collections.unmodifiableList(successfulResults);
+ }
+
+ /**
+ * Get the list of failures.
+ */
+ public List getFailures() {
+ return failures;
+ }
+
+ /**
+ * Get the list of successful results.
+ */
+ public List getSuccessfulResults() {
+ return successfulResults;
+ }
+ }
+}
diff --git a/util/src/test/java/io/kubernetes/client/util/ResourceListTest.java b/util/src/test/java/io/kubernetes/client/util/ResourceListTest.java
new file mode 100644
index 0000000000..15c9211eb5
--- /dev/null
+++ b/util/src/test/java/io/kubernetes/client/util/ResourceListTest.java
@@ -0,0 +1,225 @@
+/*
+Copyright 2024 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 org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+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.openapi.models.V1PodSpec;
+import io.kubernetes.client.openapi.models.V1Secret;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class ResourceListTest {
+
+ private ApiClient apiClient;
+
+ private static final String MULTI_RESOURCE_YAML =
+ "apiVersion: v1\n" +
+ "kind: ConfigMap\n" +
+ "metadata:\n" +
+ " name: test-configmap\n" +
+ " namespace: default\n" +
+ "data:\n" +
+ " key: value\n" +
+ "---\n" +
+ "apiVersion: v1\n" +
+ "kind: Secret\n" +
+ "metadata:\n" +
+ " name: test-secret\n" +
+ " namespace: default\n" +
+ "type: Opaque\n" +
+ "data:\n" +
+ " password: cGFzc3dvcmQ=\n";
+
+ private static final String POD_YAML =
+ "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";
+
+ @BeforeEach
+ void setup() {
+ apiClient = new ClientBuilder().setBasePath("http://localhost:8080").build();
+ }
+
+ // ========== Loading Tests ==========
+
+ @Test
+ void fromStream_singleResource_createsResourceList() throws IOException {
+ InputStream is = new ByteArrayInputStream(POD_YAML.getBytes(StandardCharsets.UTF_8));
+
+ ResourceList resourceList = ResourceList.fromStream(apiClient, is);
+
+ assertThat(resourceList).isNotNull();
+ assertThat(resourceList.getResources()).hasSize(1);
+ assertThat(resourceList.getResources().get(0)).isInstanceOf(V1Pod.class);
+ }
+
+ @Test
+ void fromStream_multipleResources_createsResourceList() throws IOException {
+ InputStream is = new ByteArrayInputStream(MULTI_RESOURCE_YAML.getBytes(StandardCharsets.UTF_8));
+
+ ResourceList resourceList = ResourceList.fromStream(apiClient, is);
+
+ assertThat(resourceList).isNotNull();
+ assertThat(resourceList.getResources()).hasSize(2);
+ assertThat(resourceList.getResources().get(0)).isInstanceOf(V1ConfigMap.class);
+ assertThat(resourceList.getResources().get(1)).isInstanceOf(V1Secret.class);
+ }
+
+ @Test
+ void fromYaml_createsResourceList() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, POD_YAML);
+
+ assertThat(resourceList).isNotNull();
+ assertThat(resourceList.getResources()).hasSize(1);
+ }
+
+ @Test
+ void getResources_returnsImmutableList() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, POD_YAML);
+ List resources = resourceList.getResources();
+
+ assertThatThrownBy(() -> resources.add(new V1Pod()))
+ .isInstanceOf(UnsupportedOperationException.class);
+ }
+
+ // ========== from() Factory Tests ==========
+
+ @Test
+ void from_withResources_createsResourceList() {
+ V1Pod pod1 = createPod("pod1", "default");
+ V1Pod pod2 = createPod("pod2", "default");
+
+ ResourceList resourceList = ResourceList.from(apiClient, pod1, pod2);
+
+ assertThat(resourceList.getResources()).hasSize(2);
+ }
+
+ @Test
+ void from_withResourceList_createsResourceList() {
+ V1Pod pod1 = createPod("pod1", "default");
+ V1Pod pod2 = createPod("pod2", "default");
+
+ ResourceList resourceList = ResourceList.from(apiClient, List.of(pod1, pod2));
+
+ assertThat(resourceList.getResources()).hasSize(2);
+ }
+
+ // ========== Utility Method Tests ==========
+
+ @Test
+ void getResources_size_returnsCorrectCount() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, MULTI_RESOURCE_YAML);
+
+ assertThat(resourceList.getResources().size()).isEqualTo(2);
+ }
+
+ @Test
+ void getResources_isEmpty_emptyList_returnsTrue() {
+ ResourceList resourceList = ResourceList.from(apiClient, List.of());
+
+ assertThat(resourceList.getResources().isEmpty()).isTrue();
+ }
+
+ @Test
+ void getResources_isEmpty_nonEmptyList_returnsFalse() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, POD_YAML);
+
+ assertThat(resourceList.getResources().isEmpty()).isFalse();
+ }
+
+ // ========== Error Handling Tests ==========
+
+ @Test
+ void fromStream_nullInput_throwsNullPointerException() {
+ assertThatThrownBy(() -> ResourceList.fromStream(apiClient, null))
+ .isInstanceOf(NullPointerException.class);
+ }
+
+ // ========== Chaining Tests ==========
+
+ @Test
+ void inNamespace_returnsThis() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, POD_YAML);
+
+ ResourceList result = resourceList.inNamespace("custom-ns");
+
+ assertThat(result).isSameAs(resourceList);
+ }
+
+ @Test
+ void continueOnError_returnsThis() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, POD_YAML);
+
+ ResourceList result = resourceList.continueOnError(true);
+
+ assertThat(result).isSameAs(resourceList);
+ }
+
+ // ========== Resource Loading and Metadata Tests ==========
+
+ @Test
+ void fromYaml_loadsConfigMap_withCorrectMetadata() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, MULTI_RESOURCE_YAML);
+
+ V1ConfigMap configMap = (V1ConfigMap) resourceList.getResources().get(0);
+ assertThat(configMap.getMetadata().getName()).isEqualTo("test-configmap");
+ assertThat(configMap.getMetadata().getNamespace()).isEqualTo("default");
+ }
+
+ @Test
+ void fromYaml_loadsSecret_withCorrectMetadata() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, MULTI_RESOURCE_YAML);
+
+ V1Secret secret = (V1Secret) resourceList.getResources().get(1);
+ assertThat(secret.getMetadata().getName()).isEqualTo("test-secret");
+ assertThat(secret.getMetadata().getNamespace()).isEqualTo("default");
+ }
+
+ @Test
+ void fromYaml_loadsPod_withCorrectMetadata() throws IOException {
+ ResourceList resourceList = ResourceList.fromYaml(apiClient, POD_YAML);
+
+ V1Pod pod = (V1Pod) resourceList.getResources().get(0);
+ assertThat(pod.getMetadata().getName()).isEqualTo("test-pod");
+ assertThat(pod.getMetadata().getNamespace()).isEqualTo("default");
+ }
+
+ private V1Pod createPod(String name, String namespace) {
+ V1ObjectMeta metadata = new V1ObjectMeta().name(name);
+ if (namespace != null) {
+ metadata.namespace(namespace);
+ }
+ return new V1Pod()
+ .apiVersion("v1")
+ .kind("Pod")
+ .metadata(metadata)
+ .spec(new V1PodSpec());
+ }
+}