diff --git a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt index eb0e643d..79c40794 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigEntries.kt @@ -100,7 +100,8 @@ data class KubeConfigNamedContext( ) { companion object { - private fun toName(user: String, cluster: String): String { + @JvmStatic + fun toName(user: String, cluster: String): String { val sanitizedUser = sanitizeName(user) val sanitizedCluster = sanitizeName(cluster) diff --git a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt index 8fc399ef..3e0a0bb3 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdate.kt @@ -16,14 +16,17 @@ import com.intellij.util.text.UniqueNameGenerator import com.redhat.devtools.gateway.auth.tls.CertificateSource import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.path import com.redhat.devtools.gateway.openshift.Utils +import io.kubernetes.client.persister.ConfigPersister import io.kubernetes.client.util.KubeConfig +import java.io.File import java.nio.file.Path abstract class KubeConfigUpdate private constructor( protected val clusterName: String, protected val clusterUrl: String, protected val token: String, - protected val allConfigs: List + protected val allConfigs: List, + private val persisterFactory: (File) -> ConfigPersister, ) { companion object { @@ -51,7 +54,45 @@ abstract class KubeConfigUpdate private constructor( abstract fun apply() - protected fun save( + protected fun saveConfigs( + primaryConfig: KubeConfig, + currentContextConfig: KubeConfig?, + currentContextName: String? + ) { + when { + currentContextConfig == null -> + saveConfig(primaryConfig) + primaryConfig.path == currentContextConfig.path -> + saveConfig(primaryConfig, currentContextName ?: primaryConfig.currentContext) + else -> { + saveConfig(primaryConfig) + saveConfig(currentContextConfig, currentContextName ?: currentContextConfig.currentContext) + } + } + } + + protected fun saveConfig(config: KubeConfig, currentContext: String? = config.currentContext) { + saveConfig( + config.contexts, + config.clusters, + config.users, + config.preferences, + currentContext, + config.path + ) + } + + protected fun saveConfig( + config: KubeConfig, + users: ArrayList, + clusters: ArrayList, + contexts: ArrayList, + currentContext: String + ) { + saveConfig(contexts, clusters, users, config.preferences, currentContext, config.path) + } + + private fun saveConfig( contexts: ArrayList?, clusters: ArrayList?, users: ArrayList?, @@ -63,7 +104,7 @@ abstract class KubeConfigUpdate private constructor( thisLogger().info("Could not write kubeconfig file. Path missing.") return } - val persister = BlockStyleFilePersister(file) + val persister = persisterFactory(file) persister.save( contexts, clusters, @@ -73,137 +114,133 @@ abstract class KubeConfigUpdate private constructor( ) } + protected data class ContextEntries( + val users: ArrayList, + val clusters: ArrayList, + val contexts: ArrayList, + val currentContextName: String + ) + + protected fun createContext( + user: KubeConfigNamedUser, + users: ArrayList?, + clusters: ArrayList?, + contexts: ArrayList?, + ): ContextEntries { + val updatedUsers = (users ?: ArrayList()).apply { + add(user.toMap()) + } + + val cluster = createCluster(allConfigs) + val updatedClusters = (clusters ?: ArrayList()).apply { + add(cluster.toMap()) + } + + val context = createContext(user, cluster, allConfigs) + val updatedContexts = (contexts ?: ArrayList()).apply { + add(context.toMap()) + } + + return ContextEntries(updatedUsers, updatedClusters, updatedContexts, context.name) + } + + protected fun uniqueUserName(allConfigs: List): String { + val existingUserNames = getAllExistingNames(allConfigs) { it.users } + return UniqueNameGenerator.generateUniqueName(clusterName, existingUserNames) + } + + private fun createCluster(allConfigs: List): KubeConfigNamedCluster { + val existingClusterNames = getAllExistingNames(allConfigs) { it.clusters } + val uniqueClusterName = UniqueNameGenerator.generateUniqueName(clusterName, existingClusterNames) + + return KubeConfigNamedCluster( + KubeConfigCluster(clusterUrl), + uniqueClusterName + ) + } + + private fun createContext( + user: KubeConfigNamedUser, + cluster: KubeConfigNamedCluster, + allConfigs: List + ): KubeConfigNamedContext { + val existingContextNames = getAllExistingNames(allConfigs) { it.contexts } + val defaultContextName = KubeConfigNamedContext.toName(user.name, cluster.name) + val uniqueContextName = UniqueNameGenerator.generateUniqueName(defaultContextName, existingContextNames) + + return KubeConfigNamedContext( + KubeConfigContext(user.name, cluster.name), + uniqueContextName + ) + } + + private fun getAllExistingNames( + allConfigs: List, + extractList: (KubeConfig) -> List<*>? + ): Set { + return allConfigs + .flatMap { config -> extractList(config) ?: emptyList() } + .mapNotNull { entryObject -> + val entryMap = entryObject as? Map<*, *> ?: return@mapNotNull null + entryMap["name"] as? String + } + .toSet() + } + class UpdateToken( clusterName: String, clusterUrl: String, token: String, private val context: KubeConfigNamedContext, - allConfigs: List - ) : KubeConfigUpdate(clusterName, clusterUrl, token, allConfigs) { + allConfigs: List, + persisterFactory: (File) -> ConfigPersister = { BlockStyleFilePersister(it) }, + ) : KubeConfigUpdate(clusterName, clusterUrl, token, allConfigs, persisterFactory) { override fun apply() { - updateToken(context.context.user) - updateCurrentContext(context.name) + val config = KubeConfigUtils.getConfigByUser(context, allConfigs) ?: return + setTokenFor(context.context.user, config) + + val currentContextConfig = KubeConfigUtils.getConfigWithCurrentContext(allConfigs) ?: config + currentContextConfig.setContext(context.name) + + saveConfigs(config, currentContextConfig, context.name) } - private fun updateToken(username: String) { - val config = KubeConfigUtils.getConfigByUser(context, allConfigs) ?: return + private fun setTokenFor(username: String, config: KubeConfig) { config.users?.find { user -> username == Utils.getValue(user, arrayOf("name")) }?.apply { Utils.setValue(this, token, arrayOf("user", "token")) - } - save( - config.contexts, - config.clusters, - config.users, - config.preferences, - config.currentContext, - config.path - ) + removeClientCerts(this) + } } - private fun updateCurrentContext(contextName: String) { - val config = KubeConfigUtils.getConfigWithCurrentContext(allConfigs) ?: return - save( - config.contexts, - config.clusters, - config.users, - config.preferences, - contextName, - config.path - ) + private fun removeClientCerts(namedUser: Any) { + Utils.removeValue(namedUser, arrayOf("user", "client-certificate-data")) + Utils.removeValue(namedUser, arrayOf("user", "client-key-data")) } + } class CreateContext( clusterName: String, clusterUrl: String, - token: String, - allConfigs: List - ) : KubeConfigUpdate(clusterName, clusterUrl, token, allConfigs) { + private val authToken: String, + allConfigs: List, + persisterFactory: (File) -> ConfigPersister = { BlockStyleFilePersister(it) }, + ) : KubeConfigUpdate(clusterName, clusterUrl, authToken, allConfigs, persisterFactory) { + override fun apply() { - // create new context in first config val config = allConfigs.firstOrNull() ?: return - - val user = createUser(allConfigs) - val users = config.users ?: ArrayList() - users.add(user.toMap()) - - val cluster = createCluster(allConfigs) - val clusters = config.clusters ?: ArrayList() - clusters.add(cluster.toMap()) - - val context = createContext(user, cluster, allConfigs) - val contexts = config.contexts ?: ArrayList() - contexts.add(context.toMap()) - - config.setContext(context.name) - - save( - contexts, - clusters, - users, - config.preferences, - config.currentContext, - config.path + val user = KubeConfigNamedUser( + KubeConfigUser(authToken), + uniqueUserName(allConfigs) ) - } + val entries = createContext(user, config.users, config.clusters, config.contexts) + config.setContext(entries.currentContextName) - private fun createUser(allConfigs: List): KubeConfigNamedUser { - val existingUserNames = getAllExistingNames(allConfigs) { it.users } - val uniqueUserName = UniqueNameGenerator.generateUniqueName(clusterName, existingUserNames) - return KubeConfigNamedUser( - KubeConfigUser(token), - uniqueUserName - ) - } - - private fun createCluster(allConfigs: List): KubeConfigNamedCluster { - val existingClusterNames = getAllExistingNames(allConfigs) { it.clusters } - val uniqueClusterName = UniqueNameGenerator.generateUniqueName(clusterName, existingClusterNames) - - return KubeConfigNamedCluster( - KubeConfigCluster(clusterUrl), - uniqueClusterName - ) - } - - private fun createContext( - user: KubeConfigNamedUser, - cluster: KubeConfigNamedCluster, - allConfigs: List - ): KubeConfigNamedContext { - val existingContextNames = getAllExistingNames(allConfigs) { it.contexts } - val tempContext = KubeConfigNamedContext( - KubeConfigContext( - user.name, - cluster.name - ) - ) - val uniqueContextName = UniqueNameGenerator.generateUniqueName(tempContext.name, existingContextNames) - - return KubeConfigNamedContext( - KubeConfigContext( - user.name, - cluster.name - ), - uniqueContextName - ) - } - - private fun getAllExistingNames( - allConfigs: List, - extractList: (KubeConfig) -> List<*>? - ): Set { - return allConfigs - .flatMap { config -> extractList(config) ?: emptyList() } - .mapNotNull { entryObject -> - val entryMap = entryObject as? Map<*, *> ?: return@mapNotNull null - entryMap["name"] as? String - } - .toSet() + saveConfig(config, entries.users, entries.clusters, entries.contexts, entries.currentContextName) } } @@ -213,47 +250,33 @@ abstract class KubeConfigUpdate private constructor( private val clientCertPem: String, private val clientKeyPem: String, private val context: KubeConfigNamedContext, - allConfigs: List - ) : KubeConfigUpdate(clusterName, clusterUrl, "", allConfigs) { + allConfigs: List, + persisterFactory: (File) -> ConfigPersister = { BlockStyleFilePersister(it) }, + ) : KubeConfigUpdate(clusterName, clusterUrl, "", allConfigs, persisterFactory) { override fun apply() { - updateClientCert(context.context.user) - updateCurrentContext(context.name) - } - - private fun updateClientCert(username: String) { val config = KubeConfigUtils.getConfigByUser(context, allConfigs) ?: return + setClientCert(config, context.context.user) + + val currentContextConfig = KubeConfigUtils.getConfigWithCurrentContext(allConfigs) ?: config + currentContextConfig.setContext(context.name) + saveConfigs(config, currentContextConfig, context.name) + } + + private fun setClientCert(config: KubeConfig, username: String) { config.users?.find { user -> username == Utils.getValue(user, arrayOf("name")) }?.apply { Utils.setValue(this, clientCertPem, arrayOf("user", "client-certificate-data")) Utils.setValue(this, clientKeyPem, arrayOf("user", "client-key-data")) - // remove token if present - Utils.removeValue(this, arrayOf("user", "token")) + removeToken(this) } - - save( - config.contexts, - config.clusters, - config.users, - config.preferences, - config.currentContext, - config.path - ) } - private fun updateCurrentContext(contextName: String) { - val config = KubeConfigUtils.getConfigWithCurrentContext(allConfigs) ?: return - save( - config.contexts, - config.clusters, - config.users, - config.preferences, - contextName, - config.path - ) + private fun removeToken(namedUser: Any) { + Utils.removeValue(namedUser, arrayOf("user", "token")) } } @@ -262,93 +285,24 @@ abstract class KubeConfigUpdate private constructor( clusterUrl: String, private val clientCertPem: String, private val clientKeyPem: String, - allConfigs: List - ) : KubeConfigUpdate(clusterName, clusterUrl, "", allConfigs) { + allConfigs: List, + persisterFactory: (File) -> ConfigPersister = { BlockStyleFilePersister(it) }, + ) : KubeConfigUpdate(clusterName, clusterUrl, "", allConfigs, persisterFactory) { override fun apply() { val config = allConfigs.firstOrNull() ?: return - - val user = createUser(allConfigs) - val users = config.users ?: ArrayList() - users.add(user.toMap()) - - val cluster = createCluster(allConfigs) - val clusters = config.clusters ?: ArrayList() - clusters.add(cluster.toMap()) - - val context = createContext(user, cluster, allConfigs) - val contexts = config.contexts ?: ArrayList() - contexts.add(context.toMap()) - - config.setContext(context.name) - - save( - contexts, - clusters, - users, - config.preferences, - config.currentContext, - config.path - ) - } - - private fun createUser(allConfigs: List): KubeConfigNamedUser { - val existingUserNames = getAllExistingNames(allConfigs) { it.users } - val uniqueUserName = - UniqueNameGenerator.generateUniqueName(clusterName, existingUserNames) - - return KubeConfigNamedUser( + val user = KubeConfigNamedUser( KubeConfigUser( token = null, clientCertificate = CertificateSource.fromData(clientCertPem), clientKey = CertificateSource.fromData(clientKeyPem) ), - uniqueUserName + uniqueUserName(allConfigs) ) - } - - private fun createCluster(allConfigs: List): KubeConfigNamedCluster { - val existingClusterNames = getAllExistingNames(allConfigs) { it.clusters } - val uniqueClusterName = - UniqueNameGenerator.generateUniqueName(clusterName, existingClusterNames) - - return KubeConfigNamedCluster( - KubeConfigCluster(clusterUrl), - uniqueClusterName - ) - } - - private fun createContext( - user: KubeConfigNamedUser, - cluster: KubeConfigNamedCluster, - allConfigs: List - ): KubeConfigNamedContext { - val existingContextNames = getAllExistingNames(allConfigs) { it.contexts } - - val tempContext = KubeConfigNamedContext( - KubeConfigContext(user.name, cluster.name) - ) - - val uniqueContextName = - UniqueNameGenerator.generateUniqueName(tempContext.name, existingContextNames) - - return KubeConfigNamedContext( - KubeConfigContext(user.name, cluster.name), - uniqueContextName - ) - } + val contextEntries = createContext(user, config.users, config.clusters, config.contexts) + config.setContext(contextEntries.currentContextName) - private fun getAllExistingNames( - allConfigs: List, - extractList: (KubeConfig) -> List<*>? - ): Set { - return allConfigs - .flatMap { config -> extractList(config) ?: emptyList() } - .mapNotNull { entryObject -> - val entryMap = entryObject as? Map<*, *> ?: return@mapNotNull null - entryMap["name"] as? String - } - .toSet() + saveConfig(config, contextEntries.users, contextEntries.clusters, contextEntries.contexts, contextEntries.currentContextName) } } } diff --git a/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt b/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt index aca5b439..e1e0092c 100644 --- a/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt +++ b/src/main/kotlin/com/redhat/devtools/gateway/openshift/OpenShiftClientFactory.kt @@ -232,7 +232,7 @@ class OpenShiftClientFactory(private val configUtils: KubeConfigUtils) { clientKey: CertificateSource? = null ): KubeConfig { - val usingToken = token != null + val usingToken = token?.isNotEmpty() == true val usingClientCert = clientCert != null && clientKey != null require(usingToken.xor(usingClientCert)) { @@ -279,32 +279,43 @@ class OpenShiftClientFactory(private val configUtils: KubeConfigUtils) { } private fun createUser(usingToken: Boolean, token: CharArray?, clientCert: CertificateSource?, clientKey: CertificateSource?): Map { - val userAuth = mutableMapOf() + val user = mutableMapOf() if (usingToken - && token != null - && token.isNotEmpty()) { - userAuth["token"] = String(token).trim() + && token != null) { + setToken(token, user) } else { - clientCert?.let { cert -> - if (cert.isFilePath) { - userAuth["client-certificate"] = cert.value.trim() - } else { - userAuth["client-certificate-data"] = PemUtils.toBase64(cert.value.trim()) - } - } - clientKey?.let { key -> - if (key.isFilePath) { - userAuth["client-key"] = key.value.trim() - } else { - userAuth["client-key-data"] = PemUtils.toBase64(key.value.trim()) - } - } + setClientCertificates(clientCert, clientKey, user) } return mapOf( "name" to userName, - "user" to userAuth + "user" to user ) } + + private fun setToken(token: CharArray, user: MutableMap) { + user["token"] = String(token).trim() + } + + private fun setClientCertificates( + clientCert: CertificateSource?, + clientKey: CertificateSource?, + user: MutableMap + ) { + clientCert?.let { cert -> + if (cert.isFilePath) { + user["client-certificate"] = cert.value.trim() + } else { + user["client-certificate-data"] = PemUtils.toBase64(cert.value.trim()) + } + } + clientKey?.let { key -> + if (key.isFilePath) { + user["client-key"] = key.value.trim() + } else { + user["client-key-data"] = PemUtils.toBase64(key.value.trim()) + } + } + } } diff --git a/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigTestHelpers.kt b/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigTestHelpers.kt index 72340efe..c70ceb1e 100644 --- a/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigTestHelpers.kt +++ b/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigTestHelpers.kt @@ -11,6 +11,7 @@ */ package com.redhat.devtools.gateway.kubeconfig +import com.redhat.devtools.gateway.auth.tls.PemUtils import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.path import io.kubernetes.client.util.KubeConfig import io.mockk.every @@ -114,6 +115,25 @@ object KubeConfigTestHelpers { ) } + /** + * Creates a user map for testing with both token and client certificates + */ + fun createUserMapWithClientCert( + name: String, + token: String, + clientCertPem: String, + clientKeyPem: String + ): MutableMap { + return mutableMapOf( + "name" to name, + "user" to mutableMapOf( + "token" to token, + "client-certificate-data" to PemUtils.toBase64(clientCertPem), + "client-key-data" to PemUtils.toBase64(clientKeyPem) + ) + ) + } + /** * Creates a cluster map for testing */ diff --git a/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt b/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt index dd2abf9b..57804cb7 100644 --- a/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt +++ b/src/test/kotlin/com/redhat/devtools/gateway/kubeconfig/KubeConfigUpdateTest.kt @@ -11,53 +11,59 @@ */ package com.redhat.devtools.gateway.kubeconfig +import com.redhat.devtools.gateway.auth.tls.PemUtils +import com.redhat.devtools.gateway.kubeconfig.KubeConfigUtils.path import com.redhat.devtools.gateway.openshift.Utils +import io.kubernetes.client.persister.ConfigPersister import io.kubernetes.client.util.KubeConfig import io.mockk.* import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import java.nio.file.Files +import java.io.File import java.nio.file.Path class KubeConfigUpdateTest { - private lateinit var tempKubeConfigFile: Path + private val kubeConfigPath = Path.of("/test/kubeconfig-primary") + private val persistersByFile = mutableMapOf() + private val testPersisterFactory: (File) -> ConfigPersister = { file -> + persistersByFile.getOrPut(file) { mockk(relaxed = true) } + } + + private fun persisterFor(path: Path): ConfigPersister { + return persistersByFile.getOrPut(path.toFile()) { mockk(relaxed = true) } + } @BeforeEach fun before() { mockkObject(KubeConfigUtils) mockkObject(Utils) - mockkConstructor(BlockStyleFilePersister::class) - every { anyConstructed().save(any(), any(), any(), any(), any()) } returns Unit - this.tempKubeConfigFile = Files.createTempFile("test-kubeconfig", ".tmp") } @AfterEach fun after() { - unmockkConstructor(BlockStyleFilePersister::class) unmockkAll() clearAllMocks() - Files.deleteIfExists(tempKubeConfigFile) } @Test fun `#apply creates the context if it does not exist`() { // given val data = CreateContextTestData() - val config = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( match { contexts -> assertThat(contexts).hasSize(1) verifyContext(contexts[0] as Map<*, *>, "${data.clusterName}/${data.clusterName}", data.clusterName, data.clusterName) @@ -76,23 +82,120 @@ class KubeConfigUpdateTest { } } + @Test + fun `#apply CreateContextWithClientCert creates the context if it does not exist`() { + // given + val data = CreateContextWithClientCertTestData() + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath) + val allConfigs = listOf(config) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) + + val update = KubeConfigUpdate.CreateContextWithClientCert( + data.clusterName, + data.clusterUrl, + data.clientCertPem, + data.clientKeyPem, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then + verify { + persisterFor(kubeConfigPath).save( + match { contexts -> + assertThat(contexts).hasSize(1) + verifyContext( + contexts[0] as Map<*, *>, + "${data.clusterName}/${data.clusterName}", + data.clusterName, + data.clusterName + ) + }, + match { clusters -> + assertThat(clusters).hasSize(1) + verifyCluster(clusters[0] as Map<*, *>, data.clusterName, data.clusterUrl) + }, + match { users -> + assertThat(users).hasSize(1) + verifyUserWithClientCert( + users[0] as Map<*, *>, + data.clusterName, + data.clientCertPem, + data.clientKeyPem + ) + }, + any(), + any(), + ) + } + } + + @Test + fun `#apply CreateContextWithClientCert generates unique user name if user name already exists`() { + // given + val data = CreateContextWithClientCertTestData() + val existingUserMap = KubeConfigTestHelpers.createUserMap(data.clusterName, "existing-token") + + val config = KubeConfigTestHelpers.createMockKubeConfig( + kubeConfigPath, + users = listOf(existingUserMap) + ) + val allConfigs = listOf(config) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) + + val update = KubeConfigUpdate.CreateContextWithClientCert( + data.clusterName, + data.clusterUrl, + data.clientCertPem, + data.clientKeyPem, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then + verify { + persisterFor(kubeConfigPath).save( + any(), + any(), + match { users -> + verifyNewEntryInList(users, 2, "${data.clusterName}2") { userMap -> + verifyUserWithClientCert( + userMap, + "${data.clusterName}2", + data.clientCertPem, + data.clientKeyPem + ) + } + }, + any(), + any(), + ) + } + } + @Test fun `#apply updates the token if context already exists`() { // given val data = UpdateTokenTestData() val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateTokenTestMaps(data) - val config = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile, existingUserMap, existingClusterMap, existingContextMap) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap) val allConfigs = listOf(config) - val mockContext = setupUpdateTokenMocks(data, allConfigs, config, null) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) - val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs) + val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( match { contexts -> assertThat(contexts).hasSize(1) verifyContext(contexts[0] as Map<*, *>, data.clusterName, data.clusterName, data.userName) @@ -116,42 +219,193 @@ class KubeConfigUpdateTest { // given val data = UpdateTokenTestData() val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateTokenTestMaps(data) - val configForToken = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile, existingUserMap, existingClusterMap, existingContextMap, "other-context") - val configForCurrentContext = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile, existingUserMap, existingClusterMap, existingContextMap, "other-context") + val configForToken = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap, "other-context") + val configForCurrentContext = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap, "other-context") val allConfigs = listOf(configForToken, configForCurrentContext) - val mockContext = setupUpdateTokenMocks(data, allConfigs, configForToken, configForCurrentContext) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, configForToken, configForCurrentContext) - val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs) + val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs, testPersisterFactory) // when update.apply() - // then - verify that save is called twice: once for token update, once for current context update - verify(exactly = 2) { - anyConstructed().save(any(), any(), any(), any(), any()) + // then - token and current-context changes are persisted in a single save + verify(exactly = 1) { + persisterFor(kubeConfigPath).save(any(), any(), any(), any(), any()) } - - // Verify both calls were made: first with original context, second with context name - // The match function returns boolean - MockK will try calls until one matches - verify(atLeast = 1) { - anyConstructed().save( + + verify { + persisterFor(kubeConfigPath).save( any(), any(), + match { users -> + assertThat(users).hasSize(1) + verifyUser(users[0] as Map<*, *>, data.userName, data.newToken) + }, any(), + match { currentContext -> + currentContext == data.contextName + }, + ) + } + } + + @Test + fun `#apply UpdateToken saves user and current-context configs separately when on different files`() { + // given + val data = UpdateTokenTestData() + val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateTokenTestMaps(data) + val userConfigFile = Path.of("/test/kubeconfig-user") + val currentContextConfigFile = Path.of("/test/kubeconfig-current-context") + val configForUser = KubeConfigTestHelpers.createMockKubeConfig( + userConfigFile, + existingUserMap, + existingClusterMap, + existingContextMap, + "" + ) + val configForCurrentContext = KubeConfigTestHelpers.createMockKubeConfig( + currentContextConfigFile, + existingUserMap, + existingClusterMap, + existingContextMap, + "other-context" + ) + val allConfigs = listOf(configForUser, configForCurrentContext) + val mockContext = setupUpdateExistingContextMocks( + data.clusterName, + data.userName, + data.contextName, + allConfigs, + configForUser, + configForCurrentContext + ) + + val update = KubeConfigUpdate.UpdateToken( + data.clusterName, + data.clusterUrl, + data.newToken, + mockContext, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then - user config and current-context config are persisted separately, each by its own persister + val userFile = userConfigFile.toFile() + val currentContextFile = currentContextConfigFile.toFile() + val userPersister = persistersByFile[userFile] + val currentContextPersister = persistersByFile[currentContextFile] + + verify(exactly = 1) { + userPersister!!.save( any(), - match { currentContext -> - currentContext == "other-context" + any(), + match { users -> + assertThat(users).hasSize(1) + verifyUser(users[0] as Map<*, *>, data.userName, data.newToken) }, + any(), + any(), ) } - - verify(atLeast = 1) { - anyConstructed().save( + + verify(exactly = 1) { + currentContextPersister!!.save( + any(), + any(), + any(), + any(), + match { currentContext -> + currentContext == data.contextName + }, + ) + } + } + + @Test + fun `#apply UpdateClientCert updates client cert if context already exists`() { + // given + val data = UpdateClientCertTestData() + val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateClientCertTestMaps(data) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap) + val allConfigs = listOf(config) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) + + val update = KubeConfigUpdate.UpdateClientCert( + data.clusterName, + data.clusterUrl, + data.clientCertPem, + data.clientKeyPem, + mockContext, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then + verify { + persisterFor(kubeConfigPath).save( + match { contexts -> + assertThat(contexts).hasSize(1) + verifyContext(contexts[0] as Map<*, *>, data.clusterName, data.clusterName, data.userName) + }, + match { clusters -> + assertThat(clusters).hasSize(1) + verifyCluster(clusters[0] as Map<*, *>, data.clusterName, data.clusterUrl) + }, + match { users -> + assertThat(users).hasSize(1) + verifyUserWithClientCert(users[0] as Map<*, *>, data.userName, data.clientCertPem, data.clientKeyPem) + }, any(), any(), + ) + } + } + + @Test + fun `#apply UpdateClientCert sets current context when updating client cert`() { + // given + val data = UpdateClientCertTestData() + val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateClientCertTestMaps(data) + val configForUser = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap, "other-context") + val configForCurrentContext = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap, "other-context") + val allConfigs = listOf(configForUser, configForCurrentContext) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, configForUser, configForCurrentContext) + + val update = KubeConfigUpdate.UpdateClientCert( + data.clusterName, + data.clusterUrl, + data.clientCertPem, + data.clientKeyPem, + mockContext, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then - cert and current-context changes are persisted in a single save + verify(exactly = 1) { + persisterFor(kubeConfigPath).save(any(), any(), any(), any(), any()) + } + + verify { + persisterFor(kubeConfigPath).save( any(), any(), - match { currentContext -> + match { users -> + assertThat(users).hasSize(1) + verifyUserWithClientCert(users[0] as Map<*, *>, data.userName, data.clientCertPem, data.clientKeyPem) + }, + any(), + match { currentContext -> currentContext == data.contextName }, ) @@ -159,22 +413,165 @@ class KubeConfigUpdateTest { } @Test - fun `#apply does not set current context when no config has current context`() { + fun `#apply UpdateClientCert sets current context on primary config when no config has current context`() { + // given + val data = UpdateClientCertTestData() + val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateClientCertTestMaps(data) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap, "") + val allConfigs = listOf(config) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) + + val update = KubeConfigUpdate.UpdateClientCert( + data.clusterName, + data.clusterUrl, + data.clientCertPem, + data.clientKeyPem, + mockContext, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then - cert and current-context are persisted in a single save on the primary config + verify(exactly = 1) { + persisterFor(kubeConfigPath).save(any(), any(), any(), any(), any()) + } + + verify { + persisterFor(kubeConfigPath).save( + any(), + any(), + match { users -> + assertThat(users).hasSize(1) + verifyUserWithClientCert(users[0] as Map<*, *>, data.userName, data.clientCertPem, data.clientKeyPem) + }, + any(), + match { currentContext -> + currentContext == data.contextName + }, + ) + } + } + + @Test + fun `#apply UpdateClientCert removes token when setting client cert`() { + // given + val data = UpdateClientCertWithTokenTestData() + val existingUserMap = KubeConfigTestHelpers.createUserMapWithClientCert( + data.userName, + data.oldToken, + data.oldClientCertPem, + data.oldClientKeyPem + ) + val existingClusterMap = KubeConfigTestHelpers.createClusterMap(data.clusterName, data.clusterUrl) + val existingContextMap = KubeConfigTestHelpers.createContextMap(data.contextName, data.clusterName, data.userName) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap) + val allConfigs = listOf(config) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) + + val update = KubeConfigUpdate.UpdateClientCert( + data.clusterName, + data.clusterUrl, + data.newClientCertPem, + data.newClientKeyPem, + mockContext, + allConfigs, + testPersisterFactory, + ) + + // when + update.apply() + + // then - token should be removed, only the new certificates should remain + verify { + persisterFor(kubeConfigPath).save( + any(), + any(), + match { users -> + assertThat(users).hasSize(1) + verifyUserWithClientCert( + users[0] as Map<*, *>, + data.userName, + data.newClientCertPem, + data.newClientKeyPem + ) + }, + any(), + any(), + ) + } + } + + @Test + fun `#apply UpdateToken sets current context on primary config when no config has current context`() { // given val data = UpdateTokenTestData() val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateTokenTestMaps(data) - val config = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile, existingUserMap, existingClusterMap, existingContextMap, "") + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap, "") val allConfigs = listOf(config) - val mockContext = setupUpdateTokenMocks(data, allConfigs, config, null) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) - val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs) + val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs, testPersisterFactory) // when update.apply() - // then - verify that save is called only once (for token update, not for current context) + // then - token and current-context are persisted in a single save on the primary config verify(exactly = 1) { - anyConstructed().save(any(), any(), any(), any(), any()) + persisterFor(kubeConfigPath).save(any(), any(), any(), any(), any()) + } + + verify { + persisterFor(kubeConfigPath).save( + any(), + any(), + match { users -> + assertThat(users).hasSize(1) + verifyUser(users[0] as Map<*, *>, data.userName, data.newToken) + }, + any(), + match { currentContext -> + currentContext == data.contextName + }, + ) + } + } + + @Test + fun `#apply UpdateToken removes client-certificate-data and client-key-data when setting a new token`() { + // given + val data = UpdateTokenWithClientCertTestData() + val existingUserMap = KubeConfigTestHelpers.createUserMapWithClientCert( + data.userName, + data.oldToken, + data.clientCertPem, + data.clientKeyPem + ) + val existingClusterMap = KubeConfigTestHelpers.createClusterMap(data.clusterName, data.clusterUrl) + val existingContextMap = KubeConfigTestHelpers.createContextMap(data.contextName, data.clusterName, data.userName) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap) + val allConfigs = listOf(config) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) + + val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs, testPersisterFactory) + + // when + update.apply() + + // then - certificates should be removed, only the new token should remain + verify { + persisterFor(kubeConfigPath).save( + any(), + any(), + match { users -> + assertThat(users).hasSize(1) + verifyUser(users[0] as Map<*, *>, data.userName, data.newToken) + }, + any(), + any(), + ) } } @@ -185,20 +582,20 @@ class KubeConfigUpdateTest { val existingUserMap = KubeConfigTestHelpers.createUserMap(data.clusterName, "existing-token") val config = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, users = listOf(existingUserMap) ) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), any(), match { users -> @@ -219,20 +616,20 @@ class KubeConfigUpdateTest { val existingClusterMap = KubeConfigTestHelpers.createClusterMap(data.clusterName, "https://existing.com") val config = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, clusters = listOf(existingClusterMap) ) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), match { clusters -> verifyNewEntryInList(clusters, 2, "${data.clusterName}2") { clusterMap -> @@ -253,20 +650,20 @@ class KubeConfigUpdateTest { val existingContextMap = KubeConfigTestHelpers.createContextMap("${data.clusterName}/${data.clusterName}", "other-cluster", "other-user") val config = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, contexts = listOf(existingContextMap) ) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( match { contexts -> verifyNewEntryInList(contexts, 2, "${data.clusterName}/${data.clusterName}2") { contextMap -> verifyContext(contextMap, "${data.clusterName}/${data.clusterName}2", data.clusterName, data.clusterName) @@ -289,20 +686,20 @@ class KubeConfigUpdateTest { val existingUser2 = KubeConfigTestHelpers.createUserMap("${data.clusterName}2", "token2") val config = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, users = listOf(existingUser1, existingUser2) ) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), any(), match { users -> @@ -323,21 +720,21 @@ class KubeConfigUpdateTest { val existingUserMap = KubeConfigTestHelpers.createUserMap(data.clusterName, "existing-token") val config1 = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, users = listOf(existingUserMap) ) - val config2 = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile) + val config2 = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath) val allConfigs = listOf(config1, config2) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then - should generate unique name even though the duplicate is in a different config verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), any(), match { users -> @@ -362,22 +759,22 @@ class KubeConfigUpdateTest { val existingContextMap = KubeConfigTestHelpers.createContextMap("${data.clusterName}/${data.clusterName}", "other-cluster", "other-user") val config = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, contexts = listOf(existingContextMap), clusters = listOf(existingClusterMap), users = listOf(existingUserMap) ) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then - all three should have unique names with suffix 2 verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( match { contexts -> verifyNewEntryInList(contexts, 2, "${data.clusterName}2/${data.clusterName}2") { contextMap -> verifyContext(contextMap, "${data.clusterName}2/${data.clusterName}2", "${data.clusterName}2", "${data.clusterName}2") @@ -406,7 +803,7 @@ class KubeConfigUpdateTest { val expectedContextName = "${data.clusterName}/${data.clusterName}" val config = KubeConfigTestHelpers.createMockKubeConfig( - tempKubeConfigFile, + kubeConfigPath, setupContextCapture = { mockConfig -> // Capture the context name set via setContext() and return it via currentContext val contextNameSlot = slot() @@ -417,16 +814,16 @@ class KubeConfigUpdateTest { } ) val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), any(), any(), @@ -436,24 +833,71 @@ class KubeConfigUpdateTest { } } + @Test + fun `#apply addNewContext persists entries when config lists are null`() { + // given + val data = CreateContextTestData() + val config = mockk(relaxed = true) + every { config.users } returns null + every { config.clusters } returns null + every { config.contexts } returns null + every { config.currentContext } answers { "" } + every { config.path } returns kubeConfigPath + every { config.preferences } returns mockk() + every { config.setContext(any()) } returns true + + val allConfigs = listOf(config) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) + + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) + + // when + update.apply() + + // then + verify { + persisterFor(kubeConfigPath).save( + match { savedContexts -> + assertThat(savedContexts).hasSize(1) + verifyContext( + savedContexts[0] as Map<*, *>, + "${data.clusterName}/${data.clusterName}", + data.clusterName, + data.clusterName + ) + }, + match { savedClusters -> + assertThat(savedClusters).hasSize(1) + verifyCluster(savedClusters[0] as Map<*, *>, data.clusterName, data.clusterUrl) + }, + match { savedUsers -> + assertThat(savedUsers).hasSize(1) + verifyUser(savedUsers[0] as Map<*, *>, data.clusterName, data.token) + }, + any(), + any(), + ) + } + } + @Test fun `#apply works when preferences are null`() { // given val data = CreateContextTestData() - val config = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath) every { config.preferences } returns null val allConfigs = listOf(config) - setupCreateContextMocks(data.clusterName, allConfigs, tempKubeConfigFile) + setupCreateContextMocks(data.clusterName, allConfigs, kubeConfigPath) - val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs) + val update = KubeConfigUpdate.CreateContext(data.clusterName, data.clusterUrl, data.token, allConfigs, testPersisterFactory) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), any(), any(), @@ -468,20 +912,56 @@ class KubeConfigUpdateTest { // given val data = UpdateTokenTestData() val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateTokenTestMaps(data) - val config = KubeConfigTestHelpers.createMockKubeConfig(tempKubeConfigFile, existingUserMap, existingClusterMap, existingContextMap) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap) every { config.preferences } returns null val allConfigs = listOf(config) - val mockContext = setupUpdateTokenMocks(data, allConfigs, config, null) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) + + val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs, testPersisterFactory) + + // when + update.apply() + + // then + verify { + persisterFor(kubeConfigPath).save( + any(), + any(), + any(), + null, + any() + ) + } + } + + @Test + fun `#apply UpdateClientCert updates client cert when preferences are null`() { + // given + val data = UpdateClientCertTestData() + val (existingUserMap, existingClusterMap, existingContextMap) = createUpdateClientCertTestMaps(data) + val config = KubeConfigTestHelpers.createMockKubeConfig(kubeConfigPath, existingUserMap, existingClusterMap, existingContextMap) + every { config.preferences } returns null - val update = KubeConfigUpdate.UpdateToken(data.clusterName, data.clusterUrl, data.newToken, mockContext, allConfigs) + val allConfigs = listOf(config) + val mockContext = setupUpdateExistingContextMocks(data.clusterName, data.userName, data.contextName, allConfigs, config, null) + + val update = KubeConfigUpdate.UpdateClientCert( + data.clusterName, + data.clusterUrl, + data.clientCertPem, + data.clientKeyPem, + mockContext, + allConfigs, + testPersisterFactory, + ) // when update.apply() // then verify { - anyConstructed().save( + persisterFor(kubeConfigPath).save( any(), any(), any(), @@ -506,6 +986,46 @@ class KubeConfigUpdateTest { val token: String = "join-the-dark-side" ) + private data class CreateContextWithClientCertTestData( + val clusterName: String = "death-star", + val clusterUrl: String = "https://death-star.com", + val clientCertPem: String = "empire-client-cert", + val clientKeyPem: String = "empire-client-key" + ) + + private data class UpdateClientCertTestData( + val oldToken: String = "use-the-force", + val clientCertPem: String = "rebel-client-cert", + val clientKeyPem: String = "rebel-client-key", + val clusterName: String = "yavin-4", + val clusterUrl: String = "https://yavin-4.com", + val userName: String = "leia-organa", + val contextName: String = clusterName + ) + + private data class UpdateTokenWithClientCertTestData( + val oldToken: String = "old-token", + val newToken: String = "new-token", + val clientCertPem: String = "existing-cert", + val clientKeyPem: String = "existing-key", + val clusterName: String = "endor", + val clusterUrl: String = "https://endor.com", + val userName: String = "lando-calrissian", + val contextName: String = clusterName + ) + + private data class UpdateClientCertWithTokenTestData( + val oldToken: String = "old-token", + val oldClientCertPem: String = "old-client-cert", + val oldClientKeyPem: String = "old-client-key", + val newClientCertPem: String = "new-client-cert", + val newClientKeyPem: String = "new-client-key", + val clusterName: String = "hoth", + val clusterUrl: String = "https://hoth.com", + val userName: String = "han-solo", + val contextName: String = clusterName + ) + // Helper functions for test verification and data creation private fun findEntryByName(list: List<*>, expectedName: String): Map<*, *>? { @@ -543,6 +1063,22 @@ class KubeConfigUpdateTest { return true } + private fun verifyUserWithClientCert( + user: Map<*, *>, + expectedName: String, + expectedCertPem: String, + expectedKeyPem: String + ): Boolean { + val name = Utils.getValue(user, arrayOf("name")) as String + val cert = Utils.getValue(user, arrayOf("user", "client-certificate-data")) as String + val key = Utils.getValue(user, arrayOf("user", "client-key-data")) as String + assertThat(name).isEqualTo(expectedName) + assertThat(cert).isEqualTo(PemUtils.toBase64(expectedCertPem)) + assertThat(key).isEqualTo(PemUtils.toBase64(expectedKeyPem)) + assertThat(Utils.getValue(user, arrayOf("user", "token"))).isNull() + return true + } + private fun verifyCurrentContext(currentContext: String?, expectedContextName: String): Boolean { assertThat(currentContext).isEqualTo(expectedContextName) return true @@ -568,20 +1104,29 @@ class KubeConfigUpdateTest { return Triple(existingUserMap, existingClusterMap, existingContextMap) } + private fun createUpdateClientCertTestMaps(data: UpdateClientCertTestData): Triple, MutableMap, MutableMap> { + val existingUserMap = KubeConfigTestHelpers.createUserMap(data.userName, data.oldToken) + val existingClusterMap = KubeConfigTestHelpers.createClusterMap(data.clusterName, data.clusterUrl) + val existingContextMap = KubeConfigTestHelpers.createContextMap(data.contextName, data.clusterName, data.userName) + return Triple(existingUserMap, existingClusterMap, existingContextMap) + } - private fun setupUpdateTokenMocks( - data: UpdateTokenTestData, + private fun setupUpdateExistingContextMocks( + clusterName: String, + userName: String, + contextName: String, allConfigs: List, configForUser: KubeConfig, configForCurrentContext: KubeConfig? ): KubeConfigNamedContext { mockkObject(KubeConfigNamedContext) val mockContext = mockk(relaxed = true) - every { mockContext.context } returns KubeConfigContext(data.userName, data.clusterName) - every { mockContext.name } returns data.contextName - every { KubeConfigNamedContext.getByClusterName(data.clusterName, allConfigs) } returns mockContext + every { mockContext.context } returns KubeConfigContext(userName, clusterName) + every { mockContext.name } returns contextName + every { KubeConfigNamedContext.getByClusterName(clusterName, allConfigs) } returns mockContext every { KubeConfigUtils.getAllConfigs(any()) } returns allConfigs - every { KubeConfigUtils.getAllConfigFiles() } returns listOf(tempKubeConfigFile) + val configFiles = allConfigs.mapNotNull { it.path }.distinct() + every { KubeConfigUtils.getAllConfigFiles() } returns configFiles every { KubeConfigUtils.getConfigByUser(mockContext, allConfigs) } returns configForUser every { KubeConfigUtils.getConfigWithCurrentContext(allConfigs) } returns configForCurrentContext return mockContext