From a8e51e7199404557909b9b8387039a4a17ee89e5 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 26 Apr 2023 11:55:02 +0800 Subject: [PATCH] NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl extends ServerSideApplicable --- CHANGELOG.md | 1 + .../dsl/ListVisitFromServerWritable.java | 7 +- ...hDeleteRecreateWaitApplicableListImpl.java | 14 ++ .../client/dsl/internal/OperationContext.java | 6 +- .../client/impl/ServerSideApplyTest.java | 128 ++++++++++++++++++ 5 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/ServerSideApplyTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a4453c15d..e4a10b61a88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Fix #5033: port forwarding for clients other than okhttp needs to specify the subprotocol * Fix #5044: disable Vert.x instance file caching * Fix #5059: Vert.x InputStreamReader uses an empty Buffer sentinel to avoid NPE +* Fix #5073: `NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl` extends `ServerSideApplicable` #### Improvements * Fix #4434: Update CronJobIT to use `batch/v1` CronJob instead diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ListVisitFromServerWritable.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ListVisitFromServerWritable.java index bd882445d03..6c2428cccaa 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ListVisitFromServerWritable.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ListVisitFromServerWritable.java @@ -17,7 +17,10 @@ import java.util.List; -public interface ListVisitFromServerWritable extends - DeletableWithOptions, CreateOrReplaceable>, FieldValidateable>> { +public interface ListVisitFromServerWritable + extends DeletableWithOptions, + CreateOrReplaceable>, + ServerSideApplicable>, + FieldValidateable>> { } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java index 6388c25ba68..2fb40cdb5e5 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java @@ -332,4 +332,18 @@ public List update() { return performOperation(Resource::update); } + @Override + public List serverSideApply() { + return performOperation(Resource::serverSideApply); + } + + @Override + public ListVisitFromServerGetDeleteRecreateWaitApplicable fieldManager(String manager) { + return newInstance(context.withFieldManager(manager)); + } + + @Override + public ListVisitFromServerGetDeleteRecreateWaitApplicable forceConflicts() { + return newInstance(context.withForceConflicts()); + } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationContext.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationContext.java index 0c9b55ce9a6..3a91bc9d233 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationContext.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationContext.java @@ -508,7 +508,11 @@ public C clientInWriteContext(Class clazz) { // operationcontext OperationContext newContext = HasMetadataOperationsImpl.defaultContext(client).withDryRun(getDryRun()) .withGracePeriodSeconds(getGracePeriodSeconds()).withPropagationPolicy(getPropagationPolicy()) - .withFieldValidation(this.fieldValidation); + .withFieldValidation(this.fieldValidation).withFieldManager(this.fieldManager); + + if (Boolean.TRUE.equals(this.forceConflicts)) { + newContext = newContext.withForceConflicts(); + } // check before setting to prevent flipping the default flag if (!Objects.equals(getNamespace(), newContext.getNamespace()) diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/ServerSideApplyTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/ServerSideApplyTest.java new file mode 100644 index 00000000000..addb49fce6a --- /dev/null +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/ServerSideApplyTest.java @@ -0,0 +1,128 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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.fabric8.kubernetes.client.impl; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.ConfigBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.base.PatchType; +import io.fabric8.kubernetes.client.http.HttpClient; +import io.fabric8.kubernetes.client.http.HttpRequest; +import io.fabric8.kubernetes.client.http.HttpRequest.Builder; +import io.fabric8.kubernetes.client.http.StandardHttpRequest; +import io.fabric8.kubernetes.client.http.TestHttpResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class ServerSideApplyTest { + + private HttpClient mockClient; + private KubernetesClient kubernetesClient; + private List builders; + + @BeforeEach + public void setUp() throws IOException { + builders = new ArrayList<>(); + this.mockClient = Mockito.mock(HttpClient.class, Mockito.RETURNS_DEEP_STUBS); + when(mockClient.sendAsync(any(), Mockito.eq(byte[].class))) + .thenReturn(CompletableFuture.completedFuture(TestHttpResponse.from(200, "{}"))); + Config config = new ConfigBuilder().withMasterUrl("https://localhost:8443/").build(); + kubernetesClient = new KubernetesClientImpl(mockClient, config); + when(mockClient.newHttpRequestBuilder()).thenAnswer(answer -> { + HttpRequest.Builder result = Mockito.mock(HttpRequest.Builder.class, Mockito.RETURNS_SELF); + when(result.build()).thenReturn(new StandardHttpRequest.Builder().uri("https://localhost:8443/").build()); + builders.add(result); + return result; + }); + } + + @AfterEach + void tearDown() { + kubernetesClient.close(); + kubernetesClient = null; + } + + @Test + void testResourceListServerSideApply() { + // Given + Pod pod = withPod("pod1"); + Service svc = new ServiceBuilder().withNewMetadata().withName("svc1").endMetadata().build(); + // When + kubernetesClient.resourceList(pod, svc).inNamespace("ns1").fieldManager("x") + .forceConflicts().serverSideApply(); + + // Then + verify(mockClient, times(2)).sendAsync(any(), any()); + assertRequest(0, "PATCH", "/api/v1/namespaces/ns1/pods/pod1", "fieldManager=x&force=true", + PatchType.SERVER_SIDE_APPLY.getContentType()); + assertRequest(1, "PATCH", "/api/v1/namespaces/ns1/services/svc1", "fieldManager=x&force=true", + PatchType.SERVER_SIDE_APPLY.getContentType()); + } + + private Pod withPod(String name) { + return new PodBuilder().withNewMetadata().withName(name).withNamespace("default").endMetadata().build(); + } + + private void assertRequest(int index, String method, String url, String queryParam, String contentType) { + ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(URL.class); + Builder mock = builders.get(index); + verify(mock).url(urlCaptor.capture()); + URL capturedURL = urlCaptor.getValue(); + assertEquals(url, capturedURL.getPath()); + + validateMethod(method, contentType, mock); + + assertEquals(queryParam, capturedURL.getQuery()); + } + + static void validateMethod(String method, String contentType, Builder mock) { + ArgumentCaptor contentTypeCaptor = ArgumentCaptor.forClass(String.class); + switch (method) { + case "DELETE": + Mockito.verify(mock).delete(contentTypeCaptor.capture(), any()); + break; + case "POST": + Mockito.verify(mock).post(contentTypeCaptor.capture(), any(String.class)); + break; + case "PUT": + Mockito.verify(mock).put(contentTypeCaptor.capture(), any()); + break; + case "PATCH": + Mockito.verify(mock).patch(contentTypeCaptor.capture(), any()); + break; + default: + break; //TODO: validate GET, but that explicit call was removed + } + + if (contentType != null) { + assertEquals(contentType, contentTypeCaptor.getValue()); + } + } +}