Skip to content

Commit bd10af2

Browse files
shawkinsmanusa
authored andcommitted
fix #4861: various changes to refine resourceVersion and replace
1 parent e1a316f commit bd10af2

File tree

15 files changed

+335
-42
lines changed

15 files changed

+335
-42
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@
4949
* Fix #4659: The SupportTestingClient interface has been deprecated. Please use one of the supports methods or getApiGroup to determine what is available on the api server.
5050
* Fix #4825: removed or deprecated/moved methods that are unrelated to the rolling timeout from ImageEditReplacePatchable. Deprecated rollout methods for timeout and edit - future versions will not support
5151
* Fix #4826: removed RequestConfig upload connection and rolling timeouts. Both were no longer used with no plans to re-introduce their usage.
52+
* Fix #4861: several breaking changes related to resourceVersion handling and the replace operation:
53+
- replace is deprecated, you should use update instead. If you set the resourceVersion to null it will not be optimistically locked
54+
- json patch methods using an item for the diff generation such as edit or patch will no longer omit the resourceVersion in the patch. If you want the patch to be unlocked, then set the resourceVersion to null on the item to be patched.
55+
- createOrReplace is deprecated, you should use server side apply instead.
56+
- internal logic to mimic an apply that modify an item prior to a json patch is deprecated - you should instead build the item to be patched off of base version, such as with the edit method.
5257

5358
### 6.4.1 (2023-01-31)
5459

junit/kubernetes-server-mock/src/test/java/io/fabric8/kubernetes/client/server/mock/crud/KubernetesCrudDispatcherPatchTest.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.fabric8.kubernetes.api.model.rbac.ClusterRoleBuilder;
2525
import io.fabric8.kubernetes.client.KubernetesClient;
2626
import io.fabric8.kubernetes.client.KubernetesClientException;
27+
import io.fabric8.kubernetes.client.dsl.NamespaceableResource;
2728
import io.fabric8.kubernetes.client.dsl.Resource;
2829
import io.fabric8.kubernetes.client.dsl.base.PatchContext;
2930
import io.fabric8.kubernetes.client.dsl.base.PatchType;
@@ -325,27 +326,24 @@ class ResourceVersion {
325326
@Test
326327
@DisplayName("JSON patch, with different resource version, should patch the resource ---->" +
327328
"Invalid Client behavior which removes the resource version from the list of operations")
328-
// TODO: This test should be converted to differentResourceVersionConflict if we address
329-
// the inconsistent behavior of the client
330-
void differentResourceVersionOk() {
329+
void differentResourceVersionConflictEdit() {
331330
// Given
332-
final ConfigMap initialCm = client.resource(new ConfigMapBuilder()
331+
client.resource(new ConfigMapBuilder()
333332
.withNewMetadata().withName("json-different-resource-version").endMetadata()
334333
.addToData("key", "value")
335334
.build())
336335
.create();
337336
// When
338-
final ConfigMap patchedCm = client.resource(new ConfigMapBuilder()
337+
final NamespaceableResource<ConfigMap> patchedCmOp = client.resource(new ConfigMapBuilder()
339338
.withNewMetadata().withName("json-different-resource-version").withResourceVersion("different").endMetadata()
340339
.addToData("key", "changed")
341-
.build())
342-
.patch();
340+
.build());
343341
// Then
344-
assertThat(patchedCm)
345-
.hasFieldOrPropertyWithValue("metadata.name", "json-different-resource-version")
346-
.extracting("metadata.resourceVersion")
347-
.isNotNull()
348-
.isNotEqualTo(initialCm.getMetadata().getResourceVersion());
342+
assertThatThrownBy(() -> patchedCmOp.patch())
343+
.asInstanceOf(InstanceOfAssertFactories.type(KubernetesClientException.class))
344+
.hasFieldOrPropertyWithValue("code", 409)
345+
.extracting(KubernetesClientException::getMessage).asString()
346+
.contains("the object has been modified;");
349347
}
350348

351349
@Test

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/CreateOrReplaceable.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ public interface CreateOrReplaceable<T> extends Replaceable<T> {
2222
* fails with a HTTP_CONFLICT, it tries to replace resource.
2323
*
2424
* @return created item returned in kubernetes api response
25+
*
26+
* @deprecated please user {@link ServerSideApplicable#serverSideApply()} or attempt a create and edit/patch operation.
2527
*/
28+
@Deprecated
2629
T createOrReplace();
2730

2831
/**

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/EditReplacePatchable.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ public interface EditReplacePatchable<T>
7272
* concurrent changes (between when you obtained your item and the current state) in an unexpected way.
7373
* <p>
7474
* Consider using edit, which allows for a known base, and a builder instead.
75+
* <p>
76+
* WARNING: For some resource types there is an attempt to make this operation more like
77+
* an apply by considering implicit server side state as not being part of the patch. This behavior will be
78+
* removed in future versions, you should instead construct the resource to be patched from a resource obtained
79+
* from the api server or use patch method that is tolerant to missing state, or
80+
* {@link ServerSideApplicable#serverSideApply()}
81+
* <p>
7582
*
7683
* @param item to be patched with patched values
7784
* @return returns deserialized version of api server response
@@ -93,6 +100,13 @@ default T patch(T item) {
93100
* You may explicitly set the {@link PatchContext#getFieldManager()} as well to override the default.
94101
* </ul>
95102
*
103+
* WARNING: For a JSON patch and some resource types there is an attempt to make this operation more like
104+
* an apply by considering implicit server side state as not being part of the patch. This behavior will be
105+
* removed in future versions, you should instead construct the resource to be patched from a resource obtained
106+
* from the api server or use patch method that is tolerant to missing state, or
107+
* {@link ServerSideApplicable#serverSideApply()}
108+
* <p>
109+
*
96110
* @param item to be patched with patched values
97111
* @param patchContext {@link PatchContext} for patch request
98112
* @return returns deserialized version of api server response
@@ -149,6 +163,13 @@ default T patch(String patch) {
149163
* <p>
150164
* Consider using edit instead.
151165
*
166+
* WARNING: For some resource types there is an attempt to make this operation more like
167+
* an apply by considering implicit server side state as not being part of the patch. This behavior will be
168+
* removed in future versions, you should instead construct the resource to be patched from a resource obtained
169+
* from the api server or use patch method that is tolerant to missing state, or
170+
* {@link ServerSideApplicable#serverSideApply()}
171+
* <p>
172+
*
152173
* @return returns deserialized version of api server response
153174
*/
154175
T patch();
@@ -169,6 +190,13 @@ default T patch(String patch) {
169190
* You may explicitly set the {@link PatchContext#getFieldManager()} as well to override the default.
170191
* </ul>
171192
*
193+
* WARNING: For a JSON patch and some resource types there is an attempt to make this operation more like
194+
* an apply by considering implicit server side state as not being part of the patch. This behavior will be
195+
* removed in future versions, you should instead construct the resource to be patched from a resource obtained
196+
* from the api server or use patch method that is tolerant to missing state, or
197+
* {@link ServerSideApplicable#serverSideApply()}
198+
* <p>
199+
*
172200
* @param patchContext {@link PatchContext} for patch request
173201
* @return returns deserialized version of api server response
174202
*/

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/ItemWritableOperation.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ public interface ItemWritableOperation<T> extends DeletableWithOptions, ItemRepl
6262
*
6363
* @param item kubernetes object
6464
* @return updated object
65-
* @deprecated please use one of patchStatus, editStatus, or replaceStatus, or a locked replace
66-
* {@link Resource#lockResourceVersion(String)}
65+
* @deprecated please use resource(item).updateStatus();
6766
*/
6867
@Deprecated
6968
T updateStatus(T item);

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/dsl/Replaceable.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,38 @@ public interface Replaceable<T> {
3030
* the latest resourceVersion from the server.
3131
*
3232
* @return returns deserialized version of api server response
33+
*
34+
* @deprecated use {@link #update()} instead
3335
*/
36+
@Deprecated
3437
T replace();
3538

3639
/**
3740
* Similar to {@link Replaceable#replace()}, but only affects the status subresource
3841
*
3942
* @return returns deserialized version of api server response
43+
*
44+
* @deprecated use {@link #updateStatus()} instead
4045
*/
46+
@Deprecated
4147
T replaceStatus();
4248

49+
/**
50+
* Similar to {@link Replaceable#update()}, but only affects the status subresource
51+
*
52+
* @return returns deserialized version of api server response
53+
*/
54+
T updateStatus();
55+
56+
/**
57+
* Update the server's state with the given item (PUT).
58+
* <p>
59+
* If the resourceVersion is on the resource, the update will be performed with optimistic locking, and may
60+
* result in a conflict (409 error). If no resourceVersion is on the resource, the latest resourceVersion will
61+
* be obtained from the server prior to the update call - which may still be a conflict in a rare circumstance.
62+
*
63+
* @return returns deserialized version of api server response
64+
*/
65+
T update();
66+
4367
}

kubernetes-client-api/src/main/java/io/fabric8/kubernetes/client/extension/ResourceAdapter.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ public T item() {
330330
return resource.item();
331331
}
332332

333+
@Override
333334
public DeletableWithOptions withTimeout(long timeout, TimeUnit unit) {
334335
return resource.withTimeout(timeout, unit);
335336
}
@@ -339,4 +340,14 @@ public DeletableWithOptions withTimeoutInMillis(long timeoutInMillis) {
339340
return withTimeout(timeoutInMillis, TimeUnit.MILLISECONDS);
340341
}
341342

343+
@Override
344+
public T update() {
345+
return resource.update();
346+
}
347+
348+
@Override
349+
public T updateStatus() {
350+
return resource.updateStatus();
351+
}
352+
342353
}

kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/BaseOperation.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1103,7 +1103,7 @@ public ExtensibleResource<T> lockResourceVersion() {
11031103

11041104
@Override
11051105
public T updateStatus(T item) {
1106-
return resource(item).lockResourceVersion().replaceStatus();
1106+
return resource(item).updateStatus();
11071107
}
11081108

11091109
@Override
@@ -1140,4 +1140,14 @@ public ExtensibleResource<T> withTimeout(long timeout, TimeUnit unit) {
11401140
return newInstance(context.withTimeout(timeout, unit));
11411141
}
11421142

1143+
@Override
1144+
public T updateStatus() {
1145+
throw new KubernetesClientException(READ_ONLY_UPDATE_EXCEPTION_MESSAGE);
1146+
}
1147+
1148+
@Override
1149+
public T update() {
1150+
throw new KubernetesClientException(READ_ONLY_UPDATE_EXCEPTION_MESSAGE);
1151+
}
1152+
11431153
}

kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/HasMetadataOperation.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public class HasMetadataOperation<T extends HasMetadata, L extends KubernetesRes
4242
public static final long DEFAULT_GRACE_PERIOD_IN_SECONDS = -1L;
4343
private static final String PATCH_OPERATION = "patch";
4444
private static final String REPLACE_OPERATION = "replace";
45+
private static final String UPDATE_OPERATION = "update";
4546

4647
public HasMetadataOperation(OperationContext ctx, Class<T> type, Class<L> listType) {
4748
super(ctx);
@@ -103,6 +104,36 @@ protected T modifyItemForReplaceOrPatch(Supplier<T> current, T item) {
103104
return item;
104105
}
105106

107+
@Override
108+
public T update() {
109+
return this.update(getItem(), false);
110+
}
111+
112+
@Override
113+
public T updateStatus() {
114+
return this.update(getItem(), true);
115+
}
116+
117+
protected T update(T item, boolean status) {
118+
String existingResourceVersion = KubernetesResourceUtil.getResourceVersion(item);
119+
try {
120+
if (existingResourceVersion == null) {
121+
T got = requireFromServer();
122+
String resourceVersion = KubernetesResourceUtil.getResourceVersion(got);
123+
item = clone(item);
124+
item.getMetadata().setResourceVersion(resourceVersion);
125+
}
126+
return handleUpdate(item, status);
127+
} catch (KubernetesClientException e) {
128+
throw KubernetesClientException.launderThrowable(forOperationType(UPDATE_OPERATION), e);
129+
} catch (InterruptedException e) {
130+
Thread.currentThread().interrupt();
131+
throw KubernetesClientException.launderThrowable(forOperationType(UPDATE_OPERATION), e);
132+
} catch (IOException e) {
133+
throw KubernetesClientException.launderThrowable(forOperationType(UPDATE_OPERATION), e);
134+
}
135+
}
136+
106137
/**
107138
* base replace operation, which is effectively a forced update with retries
108139
*/
@@ -171,17 +202,10 @@ protected T replace(T item, boolean status) {
171202
* be fetched from the server.
172203
*/
173204
protected T patch(PatchContext context, T base, T item, boolean status) {
174-
if (context == null || context.getPatchType() == PatchType.JSON) {
205+
if ((context == null || context.getPatchType() == PatchType.JSON) && base == null) {
175206
if (base == null) {
176207
base = requireFromServer();
177208
}
178-
if (base.getMetadata() != null) {
179-
// prevent the resourceVersion from being modified in the patch
180-
if (item.getMetadata() == null) {
181-
item.setMetadata(new ObjectMeta());
182-
}
183-
item.getMetadata().setResourceVersion(base.getMetadata().getResourceVersion());
184-
}
185209
final T current = base;
186210
try {
187211
item = modifyItemForReplaceOrPatch(() -> current, item);

kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/NamespaceVisitFromServerGetWatchDeleteRecreateWaitApplicableListImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,4 +322,14 @@ public DeletableWithOptions withTimeoutInMillis(long timeoutInMillis) {
322322
return this.withTimeout(timeoutInMillis, TimeUnit.MILLISECONDS);
323323
}
324324

325+
@Override
326+
public List<HasMetadata> updateStatus() {
327+
return performOperation(Resource::updateStatus);
328+
}
329+
330+
@Override
331+
public List<HasMetadata> update() {
332+
return performOperation(Resource::update);
333+
}
334+
325335
}

0 commit comments

Comments
 (0)