From 8aa4503dcfb043965062b1d245935e8b62c44eae Mon Sep 17 00:00:00 2001 From: Steve Hawkins Date: Mon, 24 Oct 2022 08:03:11 -0400 Subject: [PATCH] fix #4136: adding support for fieldValidation --- CHANGELOG.md | 1 + .../client/dsl/FieldValidateable.java | 45 ++++++++++++++ .../dsl/ListVisitFromServerWritable.java | 2 +- .../client/dsl/NonDeletingOperation.java | 23 ++++++++ .../client/dsl/WritableOperation.java | 5 +- .../client/extension/ExtensibleResource.java | 3 + .../extension/ExtensibleResourceAdapter.java | 5 ++ .../client/extension/ResourceAdapter.java | 6 ++ .../client/http/StandardHttpHeaders.java | 5 ++ .../client/http/TestHttpHeaders.java | 58 ------------------- .../client/http/TestHttpRequest.java | 2 +- .../client/http/TestHttpResponse.java | 2 +- .../client/dsl/internal/BaseOperation.java | 5 ++ ...hDeleteRecreateWaitApplicableListImpl.java | 5 ++ .../client/dsl/internal/OperationContext.java | 19 +++++- .../client/dsl/internal/OperationSupport.java | 12 +++- .../kubernetes/client/impl/DryRunTest.java | 32 +++++++++- 17 files changed, 161 insertions(+), 69 deletions(-) create mode 100644 kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FieldValidateable.java create mode 100644 kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/NonDeletingOperation.java delete mode 100644 kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpHeaders.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 178f26b2eae..68453a3c73f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ #### Dependency Upgrade #### New Features +* Fix #4136: added support for fieldValidation as a dsl method for POST/PUT/PATCH operations #### _**Note**_: Breaking changes in the API diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FieldValidateable.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FieldValidateable.java new file mode 100644 index 00000000000..73f351cfc39 --- /dev/null +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/FieldValidateable.java @@ -0,0 +1,45 @@ +/** + * 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.dsl; + +public interface FieldValidateable { + + public enum Validation { + WARN, + IGNORE, + STRICT; + + String parameterValue; + + private Validation() { + this.parameterValue = this.name().charAt(0) + this.name().toLowerCase().substring(1); + } + + public String parameterValue() { + return parameterValue; + } + } + + /** + * Instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields, + * provided that the `ServerSideFieldValidation` feature gate is also enabled. + * + * @param fieldValidation + * @return write operations where field validation is applicable. + */ + T fieldValidation(Validation fieldValidation); + +} 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 b83eb87ed0a..bd882445d03 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 @@ -18,6 +18,6 @@ import java.util.List; public interface ListVisitFromServerWritable extends - DeletableWithOptions, CreateOrReplaceable> { + DeletableWithOptions, CreateOrReplaceable>, FieldValidateable>> { } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/NonDeletingOperation.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/NonDeletingOperation.java new file mode 100644 index 00000000000..fda2e50e2ab --- /dev/null +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/NonDeletingOperation.java @@ -0,0 +1,23 @@ +/** + * 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.dsl; + +public interface NonDeletingOperation extends + CreateOrReplaceable, + EditReplacePatchable, + Replaceable, ItemReplacable, + ItemWritableOperation { +} diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/WritableOperation.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/WritableOperation.java index fd5e73699e3..8a64e2f273c 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/WritableOperation.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/WritableOperation.java @@ -16,8 +16,7 @@ package io.fabric8.kubernetes.client.dsl; public interface WritableOperation extends - CreateOrReplaceable, - EditReplacePatchable, ReplaceDeletable, - ItemWritableOperation { + NonDeletingOperation, + FieldValidateable> { } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResource.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResource.java index bd063c067a4..5f864b70ddf 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResource.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResource.java @@ -75,4 +75,7 @@ public interface ExtensibleResource extends Resource { */ T getItem(); + @Override + ExtensibleResource fieldValidation(Validation fieldValidation); + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResourceAdapter.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResourceAdapter.java index 634560d78be..da534f72c12 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResourceAdapter.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ExtensibleResourceAdapter.java @@ -101,4 +101,9 @@ public T getItem() { return resource.getItem(); } + @Override + public ExtensibleResource fieldValidation(Validation fieldValidation) { + return newInstance().init(resource.fieldValidation(fieldValidation), client); + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ResourceAdapter.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ResourceAdapter.java index 259461d3be8..1de186f89ae 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ResourceAdapter.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ResourceAdapter.java @@ -28,6 +28,7 @@ import io.fabric8.kubernetes.client.dsl.Deletable; import io.fabric8.kubernetes.client.dsl.Gettable; import io.fabric8.kubernetes.client.dsl.Informable; +import io.fabric8.kubernetes.client.dsl.NonDeletingOperation; import io.fabric8.kubernetes.client.dsl.ReplaceDeletable; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.dsl.Watchable; @@ -302,4 +303,9 @@ public T patch(PatchContext patchContext) { return resource.patch(patchContext); } + @Override + public NonDeletingOperation fieldValidation(Validation fieldValidation) { + return resource.fieldValidation(fieldValidation); + } + } diff --git a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpHeaders.java b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpHeaders.java index 67ae5272265..239e345d564 100644 --- a/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpHeaders.java +++ b/kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/http/StandardHttpHeaders.java @@ -16,6 +16,7 @@ package io.fabric8.kubernetes.client.http; import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -28,6 +29,10 @@ public class StandardHttpHeaders implements HttpHeaders { private final Map> headers; + public StandardHttpHeaders() { + this(new LinkedHashMap<>()); + } + public StandardHttpHeaders(Map> headers) { this.headers = headers; } diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpHeaders.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpHeaders.java deleted file mode 100644 index 48101b1622a..00000000000 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpHeaders.java +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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.http; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Basic {@link HttpHeaders} implementation to be used in tests instead of mocks or real headers. - * - * @param type for the return type of chained methods. - */ -public class TestHttpHeaders implements HttpHeaders { - - private final Map> headers = new HashMap<>(); - - @Override - public List headers(String key) { - return headers.getOrDefault(key, Collections.emptyList()); - } - - @Override - public Map> headers() { - return headers; - } - - public T clearHeaders() { - headers.clear(); - return (T) this; - } - - public T addHeader(String name, String value) { - headers.compute(name, (k, v) -> { - if (v == null) { - v = new ArrayList<>(); - } - v.add(value); - return v; - }); - return (T) this; - } -} diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpRequest.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpRequest.java index e7f2c4e31a8..cf291cf9e86 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpRequest.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpRequest.java @@ -20,7 +20,7 @@ /** * Basic {@link HttpRequest} implementation to be used in tests instead of mocks or real requests. */ -public class TestHttpRequest extends TestHttpHeaders implements HttpRequest { +public class TestHttpRequest extends StandardHttpHeaders implements HttpRequest { private URI uri; private String method; diff --git a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java index 15f60ee435f..fc3db136579 100644 --- a/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java +++ b/kubernetes-client-api/src/test/java/io/fabric8/kubernetes/client/http/TestHttpResponse.java @@ -23,7 +23,7 @@ * * @param type of the response body. */ -public class TestHttpResponse extends TestHttpHeaders> implements HttpResponse { +public class TestHttpResponse extends StandardHttpHeaders implements HttpResponse { private int code; private T body; diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperation.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperation.java index 28ec1394a69..dcb81422ee3 100755 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperation.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperation.java @@ -926,6 +926,11 @@ public ExtensibleResource dryRun(boolean isDryRun) { return newInstance(context.withDryRun(isDryRun)); } + @Override + public ExtensibleResource fieldValidation(Validation fieldValidation) { + return newInstance(context.withFieldValidation(fieldValidation)); + } + @Override public ExtensibleResource withIndexers(Map>> indexers) { BaseOperation result = newInstance(context); 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 324b590d013..827921752d9 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 @@ -204,6 +204,11 @@ public ListVisitFromServerWritable dryRun(boolean isDryRun) { return newInstance(this.context.withDryRun(isDryRun), namespaceVisitOperationContext); } + @Override + public ListVisitFromServerWritable fieldValidation(Validation fieldValidation) { + return newInstance(this.context.withFieldValidation(fieldValidation), namespaceVisitOperationContext); + } + @Override public List createOrReplace() { List> operations = getResources(); 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 6d2a93413e9..327bd12482f 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 @@ -20,6 +20,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.Client; import io.fabric8.kubernetes.client.Config; +import io.fabric8.kubernetes.client.dsl.FieldValidateable; +import io.fabric8.kubernetes.client.dsl.FieldValidateable.Validation; import io.fabric8.kubernetes.client.http.HttpClient; import io.fabric8.kubernetes.client.impl.BaseClient; import io.fabric8.kubernetes.client.impl.ResourceHandler; @@ -48,6 +50,7 @@ public class OperationContext { protected String name; protected boolean reloadingFromServer; protected boolean dryRun; + protected FieldValidateable.Validation fieldValidation; // Default to -1 to respect the value set in the resource or the Kubernetes default (30 seconds) protected long gracePeriodSeconds = -1L; @@ -70,7 +73,7 @@ public OperationContext(OperationContext other) { this(other.client, other.plural, other.namespace, other.name, other.apiGroupName, other.apiGroupVersion, other.item, other.labels, other.labelsNot, other.labelsIn, other.labelsNotIn, other.fields, other.fieldsNot, other.resourceVersion, other.reloadingFromServer, other.gracePeriodSeconds, other.propagationPolicy, - other.dryRun, other.selectorAsString, other.defaultNamespace); + other.dryRun, other.selectorAsString, other.defaultNamespace, other.fieldValidation); } public OperationContext(Client client, String plural, String namespace, String name, @@ -78,7 +81,7 @@ public OperationContext(Client client, String plural, String namespace, String n Map labelsNot, Map labelsIn, Map labelsNotIn, Map fields, Map fieldsNot, String resourceVersion, boolean reloadingFromServer, long gracePeriodSeconds, DeletionPropagation propagationPolicy, - boolean dryRun, String selectorAsString, boolean defaultNamespace) { + boolean dryRun, String selectorAsString, boolean defaultNamespace, FieldValidateable.Validation fieldValidation) { this.client = client; this.item = item; this.plural = plural; @@ -98,6 +101,7 @@ public OperationContext(Client client, String plural, String namespace, String n this.propagationPolicy = propagationPolicy; this.dryRun = dryRun; this.selectorAsString = selectorAsString; + this.fieldValidation = fieldValidation; } private void setFieldsNot(Map fieldsNot) { @@ -475,7 +479,7 @@ public C clientInWriteContext(Class clazz) { // operationcontext OperationContext newContext = HasMetadataOperationsImpl.defaultContext(client).withDryRun(getDryRun()) .withGracePeriodSeconds(getGracePeriodSeconds()).withPropagationPolicy(getPropagationPolicy()) - .withReloadingFromServer(isReloadingFromServer()); + .withReloadingFromServer(isReloadingFromServer()).withFieldValidation(this.fieldValidation); // check before setting to prevent flipping the default flag if (!Objects.equals(getNamespace(), newContext.getNamespace()) @@ -490,4 +494,13 @@ public Executor getExecutor() { return getClient().adapt(BaseClient.class).getExecutor(); } + public OperationContext withFieldValidation(Validation fieldValidation) { + if (this.fieldValidation == fieldValidation) { + return this; + } + final OperationContext context = new OperationContext(this); + context.fieldValidation = fieldValidation; + return context; + } + } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java index 6f70d486da5..2fce3ac742d 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/OperationSupport.java @@ -202,7 +202,11 @@ public URL getResourceUrl() throws MalformedURLException { public URL getResourceURLForWriteOperation(URL resourceURL) throws MalformedURLException { if (dryRun) { - return new URL(URLUtils.join(resourceURL.toString(), "?dryRun=All")); + resourceURL = new URL(URLUtils.join(resourceURL.toString(), "?dryRun=All")); + } + if (context.fieldValidation != null) { + resourceURL = new URL( + URLUtils.join(resourceURL.toString(), "?fieldValidation=" + context.fieldValidation.parameterValue())); } return resourceURL; } @@ -225,6 +229,8 @@ public URL getResourceURLForPatchOperation(URL resourceUrl, PatchContext patchCo } if (patchContext.getFieldValidation() != null) { url = URLUtils.join(url, "?fieldValidation=" + patchContext.getFieldValidation()); + } else if (this.context.fieldValidation != null) { + url = URLUtils.join(url, "?fieldValidation=" + this.context.fieldValidation.parameterValue()); } return new URL(url); } @@ -627,6 +633,10 @@ protected void retryWithExponentialBackoff(CompletableFuture response) { + List warnings = response.headers("Warning"); + if (warnings != null && !warnings.isEmpty()) { + LOG.warn("Recieved warning(s) from request at {}: {}", request.uri(), warnings); + } if (response.isSuccessful()) { return; } diff --git a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java index 2bc02f5b88a..ae7504bb1eb 100644 --- a/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java +++ b/kubernetes-client/src/test/java/io/fabric8/kubernetes/client/impl/DryRunTest.java @@ -23,6 +23,7 @@ import io.fabric8.kubernetes.client.Config; import io.fabric8.kubernetes.client.ConfigBuilder; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.dsl.FieldValidateable.Validation; import io.fabric8.kubernetes.client.http.HttpClient; import io.fabric8.kubernetes.client.http.HttpRequest; import io.fabric8.kubernetes.client.http.HttpRequest.Builder; @@ -190,8 +191,37 @@ void testResourceListDelete() { assertRequest(1, "DELETE", "/api/v1/namespaces/ns1/services/svc1", "dryRun=All"); } + @Test + void testCreateFieldValidation() { + // When + Pod pod = kubernetesClient.resource(withPod("pod1")).fieldValidation(Validation.WARN).create(); + // Then + verify(mockClient).sendAsync(any(), any()); + assertRequest("POST", "/api/v1/namespaces/default/pods", "fieldValidation=Warn"); + + assertNotNull(pod); + } + + @Test + void testCreateResourceListFieldValidation() { + // When + kubernetesClient.resourceList(withPod("pod1")).fieldValidation(Validation.IGNORE).create(); + // Then + verify(mockClient).sendAsync(any(), any()); + assertRequest("POST", "/api/v1/namespaces/default/pods", "fieldValidation=Ignore"); + } + + @Test + void testPatchFieldValidation() { + // When + kubernetesClient.resource(withPod("pod1")).fieldValidation(Validation.STRICT).patch(); + // Then + verify(mockClient, times(2)).sendAsync(any(), any()); + assertRequest(1, "PATCH", "/api/v1/namespaces/default/pods/pod1", "fieldValidation=Strict"); + } + private Pod withPod(String name) { - return new PodBuilder().withNewMetadata().withName(name).endMetadata().build(); + return new PodBuilder().withNewMetadata().withName(name).withNamespace("default").endMetadata().build(); } private void assertRequest(String method, String url, String queryParam) {