Skip to content
Open
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 @@ -15,10 +15,11 @@ import org.koin.core.component.KoinComponent

@GradualRetry(maxAttempts = 3)
@ControllerConfiguration(
fieldManager = "applicationreconciler",
informer =
Informer(
genericFilter = FlaisResourceReconciliationFilter::class,
)
),
)
@Workflow(
[
Expand Down
47 changes: 47 additions & 0 deletions src/main/kotlin/no/fintlabs/application/DeploymentDR.kt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ class DeploymentDR :
primary: FlaisApplication,
context: Context<FlaisApplication>,
): Deployment {
val staleEnvVarNames = staleLegacyEnvVarNames(actual, desired)
if (shouldRecreateForLegacyFieldOwnership(actual, staleEnvVarNames)) {
logger.info(
"Recreating deployment ${actual.metadata.name} to clear stale legacy SSA-owned env vars from manager(s) $LEGACY_FIELD_MANAGERS: ${staleEnvVarNames.joinToString()}",
)
handleDelete(primary, actual, context)
return handleCreate(desired, primary, context)
}

val kubernetesSerialization = context.client.kubernetesSerialization
val desiredSelector =
kubernetesSerialization.convertValue(desired.spec.selector, Map::class.java)
Expand Down Expand Up @@ -174,4 +183,42 @@ class DeploymentDR :
startsWith("/") -> this
else -> "/$this"
}

companion object {
private const val APPLY_OPERATION = "Apply"

private val LEGACY_FIELD_MANAGERS = setOf("flaisapplicationreconciler")

private const val DEFAULT_CONTAINER_NAME = "app"

internal fun shouldRecreateForLegacyFieldOwnership(
actual: Deployment,
staleEnvVarNames: Set<String>,
): Boolean {
val hasLegacyApplyManager =
actual.metadata?.managedFields.orEmpty().any {
it.operation == APPLY_OPERATION && it.manager in LEGACY_FIELD_MANAGERS
}

return hasLegacyApplyManager && staleEnvVarNames.isNotEmpty()
}

internal fun staleLegacyEnvVarNames(actual: Deployment, desired: Deployment): Set<String> {
val desiredByContainer = envNamesByContainer(desired)

return envNamesByContainer(actual)
.flatMap { (containerName, actualEnvNames) ->
val desiredEnvNames = desiredByContainer[containerName].orEmpty()
(actualEnvNames - desiredEnvNames).map { envName -> "$containerName:$envName" }
}
.toSet()
}

private fun envNamesByContainer(deployment: Deployment): Map<String, Set<String>> =
deployment.spec?.template?.spec?.containers.orEmpty().associate { container ->
val containerName = container.name ?: DEFAULT_CONTAINER_NAME
val envNames = container.env.orEmpty().mapNotNull { env -> env.name }.toSet()
containerName to envNames
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package no.fintlabs.application

import io.fabric8.kubernetes.api.model.Container
import io.fabric8.kubernetes.api.model.EnvVar
import io.fabric8.kubernetes.api.model.ManagedFieldsEntry
import io.fabric8.kubernetes.api.model.ObjectMeta
import io.fabric8.kubernetes.api.model.PodSpec
import io.fabric8.kubernetes.api.model.PodTemplateSpec
import io.fabric8.kubernetes.api.model.apps.Deployment
import io.fabric8.kubernetes.api.model.apps.DeploymentSpec
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

class DeploymentDRLegacyFieldOwnershipTest {
@Test
fun `should recreate when legacy manager owns any stale env vars`() {
val actual =
deployment(
envNames = setOf("JAVA_TOOL_OPTIONS", "MY_STALE_ENV"),
managers = setOf("flaisapplicationreconciler"),
)
val desired =
deployment(envNames = setOf("JAVA_TOOL_OPTIONS"), managers = setOf("applicationreconciler"))

val staleEnvVarNames = DeploymentDR.staleLegacyEnvVarNames(actual, desired)
assertTrue(DeploymentDR.shouldRecreateForLegacyFieldOwnership(actual, staleEnvVarNames))
}

@Test
fun `should not recreate when legacy manager is absent`() {
val actual =
deployment(
envNames = setOf("JAVA_TOOL_OPTIONS", "MY_STALE_ENV"),
managers = setOf("applicationreconciler"),
)
val desired =
deployment(envNames = setOf("JAVA_TOOL_OPTIONS"), managers = setOf("applicationreconciler"))

val staleEnvVarNames = DeploymentDR.staleLegacyEnvVarNames(actual, desired)
assertFalse(DeploymentDR.shouldRecreateForLegacyFieldOwnership(actual, staleEnvVarNames))
}

@Test
fun `should not recreate when stale env vars are absent`() {
val actual =
deployment(
envNames = setOf("JAVA_TOOL_OPTIONS", "MY_ENV"),
managers = setOf("flaisapplicationreconciler"),
)
val desired =
deployment(
envNames = setOf("JAVA_TOOL_OPTIONS", "MY_ENV"),
managers = setOf("applicationreconciler"),
)

val staleEnvVarNames = DeploymentDR.staleLegacyEnvVarNames(actual, desired)
assertFalse(DeploymentDR.shouldRecreateForLegacyFieldOwnership(actual, staleEnvVarNames))
}

@Test
fun `should include container name in stale env var identity`() {
val actual =
deployment(
envNames = setOf("JAVA_TOOL_OPTIONS", "MY_STALE_ENV"),
managers = setOf("flaisapplicationreconciler"),
containerName = "my-app",
)
val desired =
deployment(
envNames = setOf("JAVA_TOOL_OPTIONS"),
managers = setOf("applicationreconciler"),
containerName = "my-app",
)

val staleEnvVarNames = DeploymentDR.staleLegacyEnvVarNames(actual, desired)
assertTrue("my-app:MY_STALE_ENV" in staleEnvVarNames)
}

private fun deployment(
envNames: Set<String>,
managers: Set<String>,
containerName: String = "test",
): Deployment =
Deployment().apply {
metadata =
ObjectMeta().apply {
name = "test"
managedFields =
managers
.map { manager ->
ManagedFieldsEntry().apply {
this.manager = manager
operation = "Apply"
}
}
.toMutableList()
}
spec =
DeploymentSpec().apply {
template =
PodTemplateSpec().apply {
spec =
PodSpec().apply {
containers =
mutableListOf(
Container().apply {
name = containerName
env =
envNames
.map { envName -> EnvVar(envName, "v", null) }
.toMutableList()
}
)
}
}
}
}
}