Skip to content

fix: use #create/replace not #patch when resource has managed fields (#755) #756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,11 @@ open class ClusterResource protected constructor(
"Unsupported resource kind ${resource.kind} in version ${resource.apiVersion}."
)
}
val updated = context.replace(resource)
val updated = if (exists()) {
context.replace(resource)
} else {
context.create(resource)
}
set(updated)
return updated
} catch (e: KubernetesClientException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,10 @@ abstract class ActiveContext<N : HasMetadata, C : KubernetesClient>(
return singleResourceOperator.get(resource)
}

override fun create(resource: HasMetadata): HasMetadata? {
return singleResourceOperator.create(resource)
}

override fun replace(resource: HasMetadata): HasMetadata? {
return singleResourceOperator.replace(resource)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,12 +142,21 @@ interface IActiveContext<N: HasMetadata, C: KubernetesClient>: IContext {
fun get(resource: HasMetadata): HasMetadata?

/**
* Replaces the given resource on the cluster if it exists. Creates a new one if it doesn't.
* Creates the given resource on the cluster if it doesn't exist. Throws if it exists already.
*
* @param resource that shall be replaced on the cluster
* @param resource that shall be created on the cluster
*
* @return the resource that was created
*/
fun create(resource: HasMetadata): HasMetadata?

/**
* Replaces the given resource on the cluster if it exists. Throws if it doesn't.
*
* @param resource that shall be replaced on the cluster
*
* @return the resource that was replaced
*/
fun replace(resource: HasMetadata): HasMetadata?

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package com.redhat.devtools.intellij.kubernetes.model.resource
import com.redhat.devtools.intellij.kubernetes.model.client.ClientAdapter
import com.redhat.devtools.intellij.kubernetes.model.util.ResourceException
import com.redhat.devtools.intellij.kubernetes.model.util.hasGenerateName
import com.redhat.devtools.intellij.kubernetes.model.util.hasManagedFields
import com.redhat.devtools.intellij.kubernetes.model.util.hasName
import com.redhat.devtools.intellij.kubernetes.model.util.runWithoutServerSetProperties
import io.fabric8.kubernetes.api.model.APIResource
Expand All @@ -35,13 +36,17 @@ import io.fabric8.kubernetes.client.utils.Serialization
import java.net.HttpURLConnection

/**
* Offers remoting operations like [get], [replace], [watch] to
* Offers remoting operations like [get], [create], [replace], [watch] to
* retrieve, create, replace or watch a resource on the current cluster.
* API discovery is executed and a [KubernetesClientException] is thrown if resource kind and version are not supported.
*/
class NonCachingSingleResourceOperator(
private val client: ClientAdapter<out KubernetesClient>,
private val api: APIResources = APIResources(client)
private val api: APIResources = APIResources(client),
private val genericResourceFactory: (HasMetadata) -> GenericKubernetesResource = { resource ->
val yaml = Serialization.asYaml(resource)
Serialization.unmarshal(yaml, GenericKubernetesResource::class.java)
}
) {

/**
Expand Down Expand Up @@ -84,26 +89,51 @@ class NonCachingSingleResourceOperator(
val genericKubernetesResource = toGenericKubernetesResource(resource, true)
val op = createOperation(resource)
return if (hasName(genericKubernetesResource)) {
patch(genericKubernetesResource, op)
if (hasManagedFields(genericKubernetesResource)) {
patch(genericKubernetesResource, op, PatchType.STRATEGIC_MERGE)
} else {
patch(genericKubernetesResource, op, PatchType.SERVER_SIDE_APPLY)
}
} else if (hasGenerateName(genericKubernetesResource)) {
op.resource(genericKubernetesResource)
.create()
create(genericKubernetesResource, op)
} else {
throw ResourceException("Could not replace ${resource.kind ?: "resource"}: has neither name nor generateName.")
}
}

private fun patch(
fun create(resource: HasMetadata): HasMetadata? {
// force clone, patch changes the given resource
val genericKubernetesResource = toGenericKubernetesResource(resource, true)
val op = createOperation(resource)
return if (hasName(genericKubernetesResource)
&& !hasManagedFields(genericKubernetesResource)
) {
patch(genericKubernetesResource, op, PatchType.SERVER_SIDE_APPLY)
} else {
create(genericKubernetesResource, op)
}
}

private fun create(
genericKubernetesResource: GenericKubernetesResource,
op: NonNamespaceOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>>
): GenericKubernetesResource? =
runWithoutServerSetProperties(genericKubernetesResource) {
op.resource(genericKubernetesResource).create()
}

private fun patch(
genericKubernetesResource: GenericKubernetesResource,
op: NonNamespaceOperation<GenericKubernetesResource, GenericKubernetesResourceList, Resource<GenericKubernetesResource>>,
patchType: PatchType
): HasMetadata? {
return runWithoutServerSetProperties(genericKubernetesResource) {
op
.resource(genericKubernetesResource)
.patch(
PatchContext.Builder()
.withForce(true)
.withPatchType(PatchType.SERVER_SIDE_APPLY)
//.withForce(true)
.withPatchType(patchType)
.build()
)
}
Expand Down Expand Up @@ -181,8 +211,7 @@ class NonCachingSingleResourceOperator(
&& !clone) {
resource
} else {
val yaml = Serialization.asYaml(resource)
Serialization.unmarshal(yaml, GenericKubernetesResource::class.java)
genericResourceFactory.invoke(resource)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ fun hasDeletionTimestamp(resource: HasMetadata?): Boolean {
return null != resource?.metadata?.deletionTimestamp
}

fun hasManagedFields(resource: HasMetadata?): Boolean {
return true == resource?.metadata?.managedFields?.isNotEmpty()
}

fun setWillBeDeleted(resource: HasMetadata) {
setDeletionTimestamp(MARKER_WILL_BE_DELETED, resource)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,21 @@ class ClusterResourceTest {
}

@Test
fun `#push should call operator#replace`() {
fun `#push should call operator#create if resource does NOT exist`() {
// given
whenever(context.get(any()))
.doReturn(null)
.thenReturn(null)
// when
cluster.push(endorResourceOnCluster)
// then
verify(context).create(endorResourceOnCluster)
}

@Test
fun `#push should call operator#replace if resource exists`() {
// given
whenever(context.get(any()))
.thenReturn(endorResourceOnCluster)
// when
cluster.push(endorResourceOnCluster)
// then
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.never
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import com.redhat.devtools.intellij.kubernetes.model.client.KubeClientAdapter
Expand All @@ -28,6 +29,8 @@ import io.fabric8.kubernetes.api.model.APIResource
import io.fabric8.kubernetes.api.model.GenericKubernetesResource
import io.fabric8.kubernetes.api.model.GenericKubernetesResourceList
import io.fabric8.kubernetes.api.model.HasMetadata
import io.fabric8.kubernetes.api.model.ManagedFieldsEntry
import io.fabric8.kubernetes.api.model.ObjectMeta
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder
import io.fabric8.kubernetes.api.model.PodBuilder
import io.fabric8.kubernetes.client.KubernetesClientException
Expand Down Expand Up @@ -185,6 +188,84 @@ class NonCachingSingleResourceOperatorTest {
// then
}

@Test
fun `#create should call #patch(SERVER_SIDE_APPLY) if resource has a name and NO managed fields`() {
// given
val metadata = ObjectMetaBuilder().build().apply {
managedFields = null
}
val hasName = PodBuilder(namespacedCoreResource)
.withMetadata(metadata)
.build()
hasName.metadata.name = "yoda"
hasName.metadata.generateName = null
val apiResource = namespacedApiResource(namespacedCoreResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
operator.create(hasName)
// then
verify(resourceOp)
.patch(argThat(ArgumentMatcher<PatchContext> { context ->
context.patchType == PatchType.SERVER_SIDE_APPLY
}))
}

@Test
fun `#create should call #create if resource has no name`() {
// given
val hasNoName = PodBuilder(namespacedCoreResource)
.withNewMetadata()
.withManagedFields(ManagedFieldsEntry())
.endMetadata()
.build()
val apiResource = namespacedApiResource(namespacedCoreResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
operator.create(hasNoName)
// then
verify(resourceOp)
.create()
}

@Test
fun `#create should remove resourceVersion and uid before calling #create`() {
// given
val metadata = mock<ObjectMeta>()
val genericResource = mock<GenericKubernetesResource> {
on { getMetadata() } doReturn metadata
}
val hasNoName = PodBuilder(namespacedCoreResource)
.withMetadata(metadata)
.build()
val apiResource = namespacedApiResource(namespacedCoreResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
{ resource -> genericResource }
// when
operator.create(hasNoName)
// then
verify(resourceOp).create() // make sure #create was called as this only applies when #create is called
verify(metadata, times(2)).setResourceVersion(null) //
verify(metadata, times(2)).setUid(null)
}

@Test
fun `#create should call #create if resource has a name but managed fields`() {
// given
val hasNameAndManagedFields = PodBuilder(namespacedCoreResource)
.withNewMetadata()
.withManagedFields(ManagedFieldsEntry())
.withName("obiwan")
.endMetadata()
.build()
val apiResource = namespacedApiResource(namespacedCoreResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
operator.create(hasNameAndManagedFields)
// then
verify(resourceOp)
.create()
}

@Test
fun `#replace should call #inNamespace for namespaced resource`() {
// given
Expand All @@ -210,11 +291,40 @@ class NonCachingSingleResourceOperatorTest {
}

@Test
fun `#replace should call #patch() if resource has a name`() {
fun `#replace should call #patch(STRATEGIC_MERGE) if resource has a name and managed fields`() {
// given
val hasName = PodBuilder(namespacedCoreResource).build()
hasName.metadata.name = "yoda"
hasName.metadata.generateName = null
val metadata = ObjectMetaBuilder()
.withManagedFields(ManagedFieldsEntry())
.build().apply {
name = "yoda"
generateName = null
}
val hasName = PodBuilder(namespacedCoreResource)
.withMetadata(metadata)
.build()
val apiResource = namespacedApiResource(namespacedCoreResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
operator.replace(hasName)
// then
verify(resourceOp)
.patch(argThat(ArgumentMatcher<PatchContext> { context ->
context.patchType == PatchType.STRATEGIC_MERGE
}))
}

@Test
fun `#replace should call #patch(SERVER_SIDE_APPLY) if resource has a name and NO managed fields`() {
// given
val metadata = ObjectMetaBuilder()
.build().apply {
name = "yoda"
generateName = null
managedFields = null
}
val hasName = PodBuilder(namespacedCoreResource)
.withMetadata(metadata)
.build()
val apiResource = namespacedApiResource(namespacedCoreResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
Expand All @@ -229,9 +339,10 @@ class NonCachingSingleResourceOperatorTest {
@Test
fun `#replace should call #create() if resource has NO name but has generateName`() {
// given
val hasGeneratedName = PodBuilder(namespacedCoreResource).build()
hasGeneratedName.metadata.name = null
hasGeneratedName.metadata.generateName = "storm trooper clone"
val hasGeneratedName = PodBuilder(namespacedCoreResource).build().apply {
metadata.name = null
metadata.generateName = "storm trooper clone"
}
val operator = NonCachingSingleResourceOperator(
clientAdapter,
createAPIResources(namespacedApiResource(hasGeneratedName))
Expand All @@ -246,9 +357,10 @@ class NonCachingSingleResourceOperatorTest {
@Test(expected = ResourceException::class)
fun `#replace should throw if resource has NO name NOR generateName`() {
// given
val generatedName = PodBuilder(namespacedCoreResource).build()
generatedName.metadata.name = null
generatedName.metadata.generateName = null
val generatedName = PodBuilder(namespacedCoreResource).build().apply {
metadata.name = null
metadata.generateName = null
}
val apiResource = namespacedApiResource(namespacedCustomResource)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
Expand Down Expand Up @@ -306,8 +418,10 @@ class NonCachingSingleResourceOperatorTest {
@Test
fun `#watch should return NULL if resource has NO name`() {
// given
val unnamed = PodBuilder(namespacedCoreResource).build()
unnamed.metadata.name = null
val unnamed = PodBuilder(namespacedCoreResource).build().apply {
metadata.name = null
}

val apiResource = namespacedApiResource(unnamed)
val operator = NonCachingSingleResourceOperator(clientAdapter, createAPIResources(apiResource))
// when
Expand Down
Loading
Loading