Skip to content
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
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ val servletApiVersion = "4.0.1"
val oktaSpringBootVersion = "2.1.6"
val azureSpringBootBomVersion = "3.14.0"
val tikaVersion = "2.6.0"
val kubernetesClientVersion = "16.0.2"

// Tests
val jUnitBomVersion = "5.9.1"
Expand Down Expand Up @@ -173,6 +174,7 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat")
}
implementation("io.kubernetes:client-java:${kubernetesClientVersion}")

implementation("org.springdoc:springdoc-openapi-ui:${springDocVersion}")
implementation("org.springdoc:springdoc-openapi-kotlin:${springDocVersion}")
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/com/cosmotech/api/utils/KubernetesApiConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.
package com.cosmotech.api.utils

import com.cosmotech.api.config.CsmPlatformProperties
import io.kubernetes.client.openapi.apis.CoreV1Api
import io.kubernetes.client.util.ClientBuilder
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
open class KubernetesApiConfig {
@Bean
@ConditionalOnProperty(
name = ["csm.platform.runOutsideKubernetes"], havingValue = "false", matchIfMissing = true)
open fun coreV1Api(csmPlatformProperties: CsmPlatformProperties): CoreV1Api? {
val client = ClientBuilder.defaultClient()
return CoreV1Api(client)
}
}
97 changes: 97 additions & 0 deletions src/main/kotlin/com/cosmotech/api/utils/KubernetesService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.
package com.cosmotech.api.utils

import io.kubernetes.client.openapi.apis.CoreV1Api
import io.kubernetes.client.openapi.models.V1ObjectMeta
import io.kubernetes.client.openapi.models.V1Secret
import java.util.Base64
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service

private const val SECRET_LABEL = "cosmotech.com/context"

@Service
class KubernetesService(private val kubernetesApi: CoreV1Api?) : SecretManager {

private val logger = LoggerFactory.getLogger(KubernetesService::class.java)

override fun createOrReplaceSecret(
tenantName: String,
secretName: String,
secretData: Map<String, String>
) = this.createOrReplaceSecretIntoKubernetes(tenantName, secretName, secretData)

override fun readSecret(tenantName: String, secretName: String): Map<String, String> =
this.getSecretFromKubernetes(tenantName, secretName)

override fun deleteSecret(tenantName: String, secretName: String) {
this.deleteSecretFromKubernetes(tenantName, secretName)
}

private fun deleteSecretFromKubernetes(namespace: String, secretName: String) {
val api = checkKubernetesContext()
val secretNameLower = secretName.lowercase()
val labelSelector = buildLabelSector(secretNameLower)

val secrets =
api.listNamespacedSecret(
namespace, null, null, null, null, labelSelector, null, null, null, null, null)
if (secrets.items.isEmpty()) {
logger.debug("Secret does not exists in namespace $namespace: cannot delete it")
} else {
logger.info("Secret exists in namespace $namespace: deleting it")
api.deleteNamespacedSecret(secretNameLower, namespace, null, null, null, null, null, null)
}
}

private fun getSecretFromKubernetes(namespace: String, secretName: String): Map<String, String> {
val api = checkKubernetesContext()
val secretNameLower = secretName.lowercase()
val result = api.readNamespacedSecret(secretNameLower, namespace, "")

logger.debug("Secret retrieved for namespace $namespace")
return result.data?.mapValues { Base64.getDecoder().decode(it.value).toString(Charsets.UTF_8) }
?: mapOf()
}

private fun createOrReplaceSecretIntoKubernetes(
namespace: String,
secretName: String,
secretData: Map<String, String>
) {
val api = checkKubernetesContext()
logger.debug("Creating secret $secretName in namespace $namespace")

val secretNameLower = secretName.lowercase()
val labelSelector = buildLabelSector(secretNameLower)

val metadata = V1ObjectMeta()
metadata.name = secretNameLower
metadata.namespace = namespace
metadata.labels = mapOf(SECRET_LABEL to secretNameLower)
val body = V1Secret()
body.metadata = metadata

body.data = secretData.mapValues { Base64.getEncoder().encode(it.value.toByteArray()) }
body.type = "Opaque"

val secrets =
api.listNamespacedSecret(
namespace, null, null, null, null, labelSelector, null, null, null, null, null)
if (secrets.items.isEmpty()) {
logger.debug("Secret does not exists in namespace $namespace: creating it")
api.createNamespacedSecret(namespace, body, null, null, null, null)
} else {
logger.debug("Secret already exists in namespace $namespace: replacing it")
api.replaceNamespacedSecret(secretNameLower, namespace, body, null, null, null, null)
}
logger.info("Secret created/replaced")
}

private fun checkKubernetesContext(): CoreV1Api {
return this.kubernetesApi ?: throw IllegalStateException("Kubernetes API is not available")
}

private fun buildLabelSector(secretName: String) = "$SECRET_LABEL=$secretName"
}
31 changes: 31 additions & 0 deletions src/main/kotlin/com/cosmotech/api/utils/SecretManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Cosmo Tech.
// Licensed under the MIT license.
package com.cosmotech.api.utils

interface SecretManager {
/**
* Create a secret from a key/value map
*
* @param tenantName The tenant name to create the secret for
* @param secretName The secret name
* @param secretData The secret data in key/value form
*/
fun createOrReplaceSecret(tenantName: String, secretName: String, secretData: Map<String, String>)

/**
* Read a secret and return it as a key/value map
*
* @param tenantName The tenant name to create the secret for
* @param secretName The secret name
* @return The secret data in key/value form
*/
fun readSecret(tenantName: String, secretName: String): Map<String, String>

/**
* Delete a secret
*
* @param tenantName The tenant name to delete the secret for
* @param secretName The secret name
*/
fun deleteSecret(tenantName: String, secretName: String)
}