Skip to content

Commit e431b02

Browse files
committed
fix: use #create/replace not #patch when resource has managed fields (#755)
Signed-off-by: Andre Dietisheim <[email protected]>
1 parent a0c4512 commit e431b02

File tree

8 files changed

+247
-27
lines changed

8 files changed

+247
-27
lines changed

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ClusterResource.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,11 @@ open class ClusterResource protected constructor(
140140
"Unsupported resource kind ${resource.kind} in version ${resource.apiVersion}."
141141
)
142142
}
143-
val updated = context.replace(resource)
143+
val updated = if (exists()) {
144+
context.replace(resource)
145+
} else {
146+
context.create(resource)
147+
}
144148
set(updated)
145149
return updated
146150
} catch (e: KubernetesClientException) {

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/ActiveContext.kt

+4
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ abstract class ActiveContext<N : HasMetadata, C : KubernetesClient>(
266266
return singleResourceOperator.get(resource)
267267
}
268268

269+
override fun create(resource: HasMetadata): HasMetadata? {
270+
return singleResourceOperator.create(resource)
271+
}
272+
269273
override fun replace(resource: HasMetadata): HasMetadata? {
270274
return singleResourceOperator.replace(resource)
271275
}

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/context/IActiveContext.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,21 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
142142
fun get(resource: HasMetadata): HasMetadata?
143143

144144
/**
145-
* Replaces the given resource on the cluster if it exists. Creates a new one if it doesn't.
145+
* Creates the given resource on the cluster if it doesn't exist. Throws if it exists already.
146146
*
147-
* @param resource that shall be replaced on the cluster
147+
* @param resource that shall be created on the cluster
148148
*
149149
* @return the resource that was created
150150
*/
151+
fun create(resource: HasMetadata): HasMetadata?
152+
153+
/**
154+
* Replaces the given resource on the cluster if it exists. Throws if it doesn't.
155+
*
156+
* @param resource that shall be replaced on the cluster
157+
*
158+
* @return the resource that was replaced
159+
*/
151160
fun replace(resource: HasMetadata): HasMetadata?
152161

153162
/**

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/NonCachingSingleResourceOperator.kt

+39-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ package com.redhat.devtools.intellij.kubernetes.model.resource
1313
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
1414
import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException
1515
import com.redhat.devtools.intellij.kubernetes.model.util.hasGenerateName
16+
import com.redhat.devtools.intellij.kubernetes.model.util.hasManagedFields
1617
import com.redhat.devtools.intellij.kubernetes.model.util.hasName
1718
import com.redhat.devtools.intellij.kubernetes.model.util.runWithoutServerSetProperties
1819
import io.fabric8.kubernetes.api.model.APIResource
@@ -35,13 +36,17 @@ import io.fabric8.kubernetes.client.utils.Serialization
3536
import java.net.HttpURLConnection
3637

3738
/**
38-
* Offers remoting operations like [get], [replace], [watch] to
39+
* Offers remoting operations like [get], [create], [replace], [watch] to
3940
* retrieve, create, replace or watch a resource on the current cluster.
4041
* API discovery is executed and a [KubernetesClientException] is thrown if resource kind and version are not supported.
4142
*/
4243
class NonCachingSingleResourceOperator(
4344
private val client: ClientAdapter<out KubernetesClient>,
44-
private val api: APIResources = APIResources(client)
45+
private val api: APIResources = APIResources(client),
46+
private val genericResourceFactory: (HasMetadata) -> GenericKubernetesResource = { resource ->
47+
val yaml = Serialization.asYaml(resource)
48+
Serialization.unmarshal(yaml, GenericKubernetesResource::class.java)
49+
}
4550
) {
4651

4752
/**
@@ -84,26 +89,51 @@ class NonCachingSingleResourceOperator(
8489
val genericKubernetesResource = toGenericKubernetesResource(resource, true)
8590
val op = createOperation(resource)
8691
return if (hasName(genericKubernetesResource)) {
87-
patch(genericKubernetesResource, op)
92+
if (hasManagedFields(genericKubernetesResource)) {
93+
patch(genericKubernetesResource, op, PatchType.STRATEGIC_MERGE)
94+
} else {
95+
patch(genericKubernetesResource, op, PatchType.SERVER_SIDE_APPLY)
96+
}
8897
} else if (hasGenerateName(genericKubernetesResource)) {
89-
op.resource(genericKubernetesResource)
90-
.create()
98+
create(genericKubernetesResource, op)
9199
} else {
92100
throw ResourceException("Could not replace ${resource.kind ?: "resource"}: has neither name nor generateName.")
93101
}
94102
}
95103

96-
private fun patch(
104+
fun create(resource: HasMetadata): HasMetadata? {
105+
// force clone, patch changes the given resource
106+
val genericKubernetesResource = toGenericKubernetesResource(resource, true)
107+
val op = createOperation(resource)
108+
return if (hasName(genericKubernetesResource)
109+
&& !hasManagedFields(genericKubernetesResource)
110+
) {
111+
patch(genericKubernetesResource, op, PatchType.SERVER_SIDE_APPLY)
112+
} else {
113+
create(genericKubernetesResource, op)
114+
}
115+
}
116+
117+
private fun create(
97118
genericKubernetesResource: GenericKubernetesResource,
98119
op: NonNamespaceOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>>
120+
): GenericKubernetesResource? =
121+
runWithoutServerSetProperties(genericKubernetesResource) {
122+
op.resource(genericKubernetesResource).create()
123+
}
124+
125+
private fun patch(
126+
genericKubernetesResource: GenericKubernetesResource,
127+
op: NonNamespaceOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>>,
128+
patchType: PatchType
99129
): HasMetadata? {
100130
return runWithoutServerSetProperties(genericKubernetesResource) {
101131
op
102132
.resource(genericKubernetesResource)
103133
.patch(
104134
PatchContext.Builder()
105-
.withForce(true)
106-
.withPatchType(PatchType.SERVER_SIDE_APPLY)
135+
//.withForce(true)
136+
.withPatchType(patchType)
107137
.build()
108138
)
109139
}
@@ -181,8 +211,7 @@ class NonCachingSingleResourceOperator(
181211
&& !clone) {
182212
resource
183213
} else {
184-
val yaml = Serialization.asYaml(resource)
185-
Serialization.unmarshal(yaml, GenericKubernetesResource::class.java)
214+
genericResourceFactory.invoke(resource)
186215
}
187216
}
188217

src/main/kotlin/com/redhat/devtools/intellij/kubernetes/model/util/ResourceUtils.kt

+4
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,10 @@ fun hasDeletionTimestamp(resource: HasMetadata?): Boolean {
172172
return null != resource?.metadata?.deletionTimestamp
173173
}
174174

175+
fun hasManagedFields(resource: HasMetadata?): Boolean {
176+
return true == resource?.metadata?.managedFields?.isNotEmpty()
177+
}
178+
175179
fun setWillBeDeleted(resource: HasMetadata) {
176180
setDeletionTimestamp(MARKER_WILL_BE_DELETED, resource)
177181
}

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/editor/ClusterResourceTest.kt

+13-2
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,21 @@ class ClusterResourceTest {
125125
}
126126

127127
@Test
128-
fun `#push should call operator#replace`() {
128+
fun `#push should call operator#create if resource does NOT exist`() {
129129
// given
130130
whenever(context.get(any()))
131-
.doReturn(null)
131+
.thenReturn(null)
132+
// when
133+
cluster.push(endorResourceOnCluster)
134+
// then
135+
verify(context).create(endorResourceOnCluster)
136+
}
137+
138+
@Test
139+
fun `#push should call operator#replace if resource exists`() {
140+
// given
141+
whenever(context.get(any()))
142+
.thenReturn(endorResourceOnCluster)
132143
// when
133144
cluster.push(endorResourceOnCluster)
134145
// then

src/test/kotlin/com/redhat/devtools/intellij/kubernetes/model/resource/NonCachingSingleResourceOperatorTest.kt

+126-12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.nhaarman.mockitokotlin2.doReturn
1818
import com.nhaarman.mockitokotlin2.mock
1919
import com.nhaarman.mockitokotlin2.never
2020
import com.nhaarman.mockitokotlin2.spy
21+
import com.nhaarman.mockitokotlin2.times
2122
import com.nhaarman.mockitokotlin2.verify
2223
import com.nhaarman.mockitokotlin2.whenever
2324
import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter
@@ -28,6 +29,8 @@ import io.fabric8.kubernetes.api.model.APIResource
2829
import io.fabric8.kubernetes.api.model.GenericKubernetesResource
2930
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList
3031
import io.fabric8.kubernetes.api.model.HasMetadata
32+
import io.fabric8.kubernetes.api.model.ManagedFieldsEntry
33+
import io.fabric8.kubernetes.api.model.ObjectMeta
3134
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder
3235
import io.fabric8.kubernetes.api.model.PodBuilder
3336
import io.fabric8.kubernetes.client.KubernetesClientException
@@ -185,6 +188,84 @@ class NonCachingSingleResourceOperatorTest {
185188
// then
186189
}
187190

191+
@Test
192+
fun `#create should call #patch(SERVER_SIDE_APPLY) if resource has a name and NO managed fields`() {
193+
// given
194+
val metadata = ObjectMetaBuilder().build().apply {
195+
managedFields = null
196+
}
197+
val hasName = PodBuilder(namespacedCoreResource)
198+
.withMetadata(metadata)
199+
.build()
200+
hasName.metadata.name = "yoda"
201+
hasName.metadata.generateName = null
202+
val apiResource = namespacedApiResource(namespacedCoreResource)
203+
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
204+
// when
205+
operator.create(hasName)
206+
// then
207+
verify(resourceOp)
208+
.patch(argThat(ArgumentMatcher<PatchContext> { context ->
209+
context.patchType == PatchType.SERVER_SIDE_APPLY
210+
}))
211+
}
212+
213+
@Test
214+
fun `#create should call #create if resource has no name`() {
215+
// given
216+
val hasNoName = PodBuilder(namespacedCoreResource)
217+
.withNewMetadata()
218+
.withManagedFields(ManagedFieldsEntry())
219+
.endMetadata()
220+
.build()
221+
val apiResource = namespacedApiResource(namespacedCoreResource)
222+
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
223+
// when
224+
operator.create(hasNoName)
225+
// then
226+
verify(resourceOp)
227+
.create()
228+
}
229+
230+
@Test
231+
fun `#create should remove resourceVersion and uid before calling #create`() {
232+
// given
233+
val metadata = mock<ObjectMeta>()
234+
val genericResource = mock<GenericKubernetesResource> {
235+
on { getMetadata() } doReturn metadata
236+
}
237+
val hasNoName = PodBuilder(namespacedCoreResource)
238+
.withMetadata(metadata)
239+
.build()
240+
val apiResource = namespacedApiResource(namespacedCoreResource)
241+
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
242+
{ resource -> genericResource }
243+
// when
244+
operator.create(hasNoName)
245+
// then
246+
verify(resourceOp).create() // make sure #create was called as this only applies when #create is called
247+
verify(metadata, times(2)).setResourceVersion(null) //
248+
verify(metadata, times(2)).setUid(null)
249+
}
250+
251+
@Test
252+
fun `#create should call #create if resource has a name but managed fields`() {
253+
// given
254+
val hasNameAndManagedFields = PodBuilder(namespacedCoreResource)
255+
.withNewMetadata()
256+
.withManagedFields(ManagedFieldsEntry())
257+
.withName("obiwan")
258+
.endMetadata()
259+
.build()
260+
val apiResource = namespacedApiResource(namespacedCoreResource)
261+
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
262+
// when
263+
operator.create(hasNameAndManagedFields)
264+
// then
265+
verify(resourceOp)
266+
.create()
267+
}
268+
188269
@Test
189270
fun `#replace should call #inNamespace for namespaced resource`() {
190271
// given
@@ -210,11 +291,40 @@ class NonCachingSingleResourceOperatorTest {
210291
}
211292

212293
@Test
213-
fun `#replace should call #patch() if resource has a name`() {
294+
fun `#replace should call #patch(STRATEGIC_MERGE) if resource has a name and managed fields`() {
214295
// given
215-
val hasName = PodBuilder(namespacedCoreResource).build()
216-
hasName.metadata.name = "yoda"
217-
hasName.metadata.generateName = null
296+
val metadata = ObjectMetaBuilder()
297+
.withManagedFields(ManagedFieldsEntry())
298+
.build().apply {
299+
name = "yoda"
300+
generateName = null
301+
}
302+
val hasName = PodBuilder(namespacedCoreResource)
303+
.withMetadata(metadata)
304+
.build()
305+
val apiResource = namespacedApiResource(namespacedCoreResource)
306+
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
307+
// when
308+
operator.replace(hasName)
309+
// then
310+
verify(resourceOp)
311+
.patch(argThat(ArgumentMatcher<PatchContext> { context ->
312+
context.patchType == PatchType.STRATEGIC_MERGE
313+
}))
314+
}
315+
316+
@Test
317+
fun `#replace should call #patch(SERVER_SIDE_APPLY) if resource has a name and NO managed fields`() {
318+
// given
319+
val metadata = ObjectMetaBuilder()
320+
.build().apply {
321+
name = "yoda"
322+
generateName = null
323+
managedFields = null
324+
}
325+
val hasName = PodBuilder(namespacedCoreResource)
326+
.withMetadata(metadata)
327+
.build()
218328
val apiResource = namespacedApiResource(namespacedCoreResource)
219329
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
220330
// when
@@ -229,9 +339,10 @@ class NonCachingSingleResourceOperatorTest {
229339
@Test
230340
fun `#replace should call #create() if resource has NO name but has generateName`() {
231341
// given
232-
val hasGeneratedName = PodBuilder(namespacedCoreResource).build()
233-
hasGeneratedName.metadata.name = null
234-
hasGeneratedName.metadata.generateName = "storm trooper clone"
342+
val hasGeneratedName = PodBuilder(namespacedCoreResource).build().apply {
343+
metadata.name = null
344+
metadata.generateName = "storm trooper clone"
345+
}
235346
val operator = NonCachingSingleResourceOperator(
236347
clientAdapter,
237348
createAPIResources(namespacedApiResource(hasGeneratedName))
@@ -246,9 +357,10 @@ class NonCachingSingleResourceOperatorTest {
246357
@Test(expected = ResourceException::class)
247358
fun `#replace should throw if resource has NO name NOR generateName`() {
248359
// given
249-
val generatedName = PodBuilder(namespacedCoreResource).build()
250-
generatedName.metadata.name = null
251-
generatedName.metadata.generateName = null
360+
val generatedName = PodBuilder(namespacedCoreResource).build().apply {
361+
metadata.name = null
362+
metadata.generateName = null
363+
}
252364
val apiResource = namespacedApiResource(namespacedCustomResource)
253365
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
254366
// when
@@ -306,8 +418,10 @@ class NonCachingSingleResourceOperatorTest {
306418
@Test
307419
fun `#watch should return NULL if resource has NO name`() {
308420
// given
309-
val unnamed = PodBuilder(namespacedCoreResource).build()
310-
unnamed.metadata.name = null
421+
val unnamed = PodBuilder(namespacedCoreResource).build().apply {
422+
metadata.name = null
423+
}
424+
311425
val apiResource = namespacedApiResource(unnamed)
312426
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
313427
// when

0 commit comments

Comments
 (0)