Skip to content

Commit

Permalink
fix fabric8io#4136: adding support for fieldValidation
Browse files Browse the repository at this point in the history
  • Loading branch information
shawkins committed Oct 24, 2022
1 parent 3af926c commit f327644
Show file tree
Hide file tree
Showing 17 changed files with 161 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,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

Expand Down
@@ -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<T> {

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);

}
Expand Up @@ -18,6 +18,6 @@
import java.util.List;

public interface ListVisitFromServerWritable<T> extends
DeletableWithOptions, CreateOrReplaceable<List<T>> {
DeletableWithOptions, CreateOrReplaceable<List<T>>, FieldValidateable<CreateOrReplaceable<List<T>>> {

}
@@ -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<T> extends
CreateOrReplaceable<T>,
EditReplacePatchable<T>,
Replaceable<T>, ItemReplacable<T>,
ItemWritableOperation<T> {
}
Expand Up @@ -16,8 +16,7 @@
package io.fabric8.kubernetes.client.dsl;

public interface WritableOperation<T> extends
CreateOrReplaceable<T>,
EditReplacePatchable<T>,
ReplaceDeletable<T>,
ItemWritableOperation<T> {
NonDeletingOperation<T>,
FieldValidateable<NonDeletingOperation<T>> {
}
Expand Up @@ -75,4 +75,7 @@ public interface ExtensibleResource<T> extends Resource<T> {
*/
T getItem();

@Override
ExtensibleResource<T> fieldValidation(Validation fieldValidation);

}
Expand Up @@ -101,4 +101,9 @@ public T getItem() {
return resource.getItem();
}

@Override
public ExtensibleResource<T> fieldValidation(Validation fieldValidation) {
return newInstance().init(resource.fieldValidation(fieldValidation), client);
}

}
Expand Up @@ -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;
Expand Down Expand Up @@ -302,4 +303,9 @@ public T patch(PatchContext patchContext) {
return resource.patch(patchContext);
}

@Override
public NonDeletingOperation<T> fieldValidation(Validation fieldValidation) {
return resource.fieldValidation(fieldValidation);
}

}
Expand Up @@ -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;

Expand All @@ -28,6 +29,10 @@ public class StandardHttpHeaders implements HttpHeaders {

private final Map<String, List<String>> headers;

public StandardHttpHeaders() {
this(new LinkedHashMap<>());
}

public StandardHttpHeaders(Map<String, List<String>> headers) {
this.headers = headers;
}
Expand Down

This file was deleted.

Expand Up @@ -20,7 +20,7 @@
/**
* Basic {@link HttpRequest} implementation to be used in tests instead of mocks or real requests.
*/
public class TestHttpRequest extends TestHttpHeaders<TestHttpRequest> implements HttpRequest {
public class TestHttpRequest extends StandardHttpHeaders implements HttpRequest {

private URI uri;
private String method;
Expand Down
Expand Up @@ -23,7 +23,7 @@
*
* @param <T> type of the response body.
*/
public class TestHttpResponse<T> extends TestHttpHeaders<TestHttpResponse<T>> implements HttpResponse<T> {
public class TestHttpResponse<T> extends StandardHttpHeaders implements HttpResponse<T> {

private int code;
private T body;
Expand Down
Expand Up @@ -926,6 +926,11 @@ public ExtensibleResource<T> dryRun(boolean isDryRun) {
return newInstance(context.withDryRun(isDryRun));
}

@Override
public ExtensibleResource<T> fieldValidation(Validation fieldValidation) {
return newInstance(context.withFieldValidation(fieldValidation));
}

@Override
public ExtensibleResource<T> withIndexers(Map<String, Function<T, List<String>>> indexers) {
BaseOperation<T, L, R> result = newInstance(context);
Expand Down
Expand Up @@ -204,6 +204,11 @@ public ListVisitFromServerWritable<HasMetadata> dryRun(boolean isDryRun) {
return newInstance(this.context.withDryRun(isDryRun), namespaceVisitOperationContext);
}

@Override
public ListVisitFromServerWritable<HasMetadata> fieldValidation(Validation fieldValidation) {
return newInstance(this.context.withFieldValidation(fieldValidation), namespaceVisitOperationContext);
}

@Override
public List<HasMetadata> createOrReplace() {
List<? extends Resource<HasMetadata>> operations = getResources();
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -70,15 +73,15 @@ 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,
String apiGroupName, String apiGroupVersion, Object item, Map<String, String> labels,
Map<String, String[]> labelsNot, Map<String, String[]> labelsIn, Map<String, String[]> labelsNotIn,
Map<String, String> fields, Map<String, String[]> 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;
Expand All @@ -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<String, String[]> fieldsNot) {
Expand Down Expand Up @@ -475,7 +479,7 @@ public <C extends Client> C clientInWriteContext(Class<C> 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())
Expand All @@ -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;
}

}
Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -627,6 +633,10 @@ protected void retryWithExponentialBackoff(CompletableFuture<HttpResponse<byte[]
* @param response The {@link HttpResponse} object.
*/
protected void assertResponseCode(HttpRequest request, HttpResponse<?> response) {
List<String> warnings = response.headers("Warning");
if (warnings != null && !warnings.isEmpty()) {
LOG.warn("Recieved warning(s) from request at {}: {}", request.uri(), warnings);
}
if (response.isSuccessful()) {
return;
}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit f327644

Please sign in to comment.