|
| 1 | +package com.wafflestudio.spring.ocivault.config |
| 2 | + |
| 3 | +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper |
| 4 | +import com.fasterxml.jackson.module.kotlin.readValue |
| 5 | +import com.oracle.bmc.ClientConfiguration |
| 6 | +import com.oracle.bmc.ConfigFileReader |
| 7 | +import com.oracle.bmc.Region |
| 8 | +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider |
| 9 | +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider |
| 10 | +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider |
| 11 | +import com.oracle.bmc.http.ClientConfigurator |
| 12 | +import com.oracle.bmc.http.client.StandardClientProperties |
| 13 | +import com.oracle.bmc.retrier.RetryConfiguration |
| 14 | +import com.oracle.bmc.secrets.SecretsClient |
| 15 | +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails |
| 16 | +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest |
| 17 | +import com.oracle.bmc.waiter.FixedTimeDelayStrategy |
| 18 | +import com.oracle.bmc.waiter.MaxAttemptsTerminationStrategy |
| 19 | +import org.slf4j.LoggerFactory |
| 20 | +import org.springframework.boot.EnvironmentPostProcessor |
| 21 | +import org.springframework.boot.SpringApplication |
| 22 | +import org.springframework.core.env.ConfigurableEnvironment |
| 23 | +import org.springframework.core.env.MapPropertySource |
| 24 | +import org.springframework.core.env.getProperty |
| 25 | +import java.time.Duration |
| 26 | +import java.util.Base64 |
| 27 | + |
| 28 | +class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { |
| 29 | + private val log = LoggerFactory.getLogger(javaClass) |
| 30 | + private val objectMapper = jacksonObjectMapper() |
| 31 | + |
| 32 | + private val ociTimeout: Duration = Duration.ofSeconds(10) |
| 33 | + |
| 34 | + override fun postProcessEnvironment( |
| 35 | + environment: ConfigurableEnvironment, |
| 36 | + application: SpringApplication, |
| 37 | + ) { |
| 38 | + val isAotProcessing = environment.getProperty<Boolean>("spring.aot.processing", false) |
| 39 | + if (isAotProcessing) { |
| 40 | + return |
| 41 | + } |
| 42 | + val secretIdsProperty = environment.getProperty("oci.vault.secret-ids") ?: return |
| 43 | + val secretIds = secretIdsProperty.split(",").map { it.trim() } |
| 44 | + val region = |
| 45 | + Region.fromRegionId( |
| 46 | + environment.getProperty("oci.vault.region", "ap-chuncheon-1"), |
| 47 | + ) |
| 48 | + |
| 49 | + val maxAttempts = environment.getProperty<Int>("oci.retry.max-attempts", 2).coerceAtLeast(1) |
| 50 | + val retryDelayMillis = environment.getProperty<Int>("oci.retry.delay-millis", 0).coerceAtLeast(0).toLong() |
| 51 | + val retryConfiguration = |
| 52 | + RetryConfiguration.builder() |
| 53 | + .terminationStrategy(MaxAttemptsTerminationStrategy(maxAttempts)) |
| 54 | + .delayStrategy(FixedTimeDelayStrategy(retryDelayMillis)) |
| 55 | + .build() |
| 56 | + |
| 57 | + val authProvider = createAuthProvider(environment) |
| 58 | + val client = |
| 59 | + SecretsClient |
| 60 | + .builder() |
| 61 | + .configuration( |
| 62 | + ClientConfiguration |
| 63 | + .builder() |
| 64 | + .connectionTimeoutMillis(ociTimeout.toMillis().toInt()) |
| 65 | + .readTimeoutMillis(ociTimeout.toMillis().toInt()) |
| 66 | + .retryConfiguration(retryConfiguration) |
| 67 | + .build(), |
| 68 | + ) |
| 69 | + .region(region) |
| 70 | + .build(authProvider) |
| 71 | + val secrets = mutableMapOf<String, Any>() |
| 72 | + |
| 73 | + client.use { client -> |
| 74 | + secretIds.forEach { secretId -> |
| 75 | + val secretString = getSecretString(client, secretId) |
| 76 | + val parsedSecrets = objectMapper.readValue<Map<String, Any>>(secretString) |
| 77 | + secrets.putAll( |
| 78 | + parsedSecrets.filterKeys { |
| 79 | + environment.getProperty(it).isNullOrEmpty() |
| 80 | + }, |
| 81 | + ) |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + if (secrets.isNotEmpty()) { |
| 86 | + environment.propertySources.addFirst( |
| 87 | + MapPropertySource("oci-vault-secrets", secrets), |
| 88 | + ) |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { |
| 93 | + val instancePrincipalTimeoutConfigurator = |
| 94 | + ClientConfigurator { builder -> |
| 95 | + builder.property(StandardClientProperties.CONNECT_TIMEOUT, ociTimeout) |
| 96 | + builder.property(StandardClientProperties.READ_TIMEOUT, ociTimeout) |
| 97 | + } |
| 98 | + |
| 99 | + val timeoutForEachRetryMillis = |
| 100 | + environment.getProperty<Int>("oci.auth.timeout-for-each-retry-millis", ociTimeout.toMillis().toInt()) |
| 101 | + val detectEndpointRetries = environment.getProperty<Int>("oci.auth.detect-endpoint-retries", 1) |
| 102 | + |
| 103 | + return when (val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase()) { |
| 104 | + "auto" -> { |
| 105 | + try { |
| 106 | + createConfigAuthProvider(environment) |
| 107 | + } catch (e: Exception) { |
| 108 | + log.info("OCI config file auth failed; falling back to instance principal auth (oci.auth.type=auto).", e) |
| 109 | + InstancePrincipalsAuthenticationDetailsProvider |
| 110 | + .builder() |
| 111 | + .federationClientConfigurator(instancePrincipalTimeoutConfigurator) |
| 112 | + .detectEndpointRetries(detectEndpointRetries) |
| 113 | + .timeoutForEachRetry(timeoutForEachRetryMillis) |
| 114 | + .build() |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + "config", |
| 119 | + "configfile", |
| 120 | + "config_file", |
| 121 | + "config-file", |
| 122 | + -> createConfigAuthProvider(environment) |
| 123 | + |
| 124 | + "instance_principal", |
| 125 | + "instanceprincipal", |
| 126 | + "instance-principal", |
| 127 | + "ip", |
| 128 | + -> |
| 129 | + InstancePrincipalsAuthenticationDetailsProvider |
| 130 | + .builder() |
| 131 | + .federationClientConfigurator(instancePrincipalTimeoutConfigurator) |
| 132 | + .detectEndpointRetries(detectEndpointRetries) |
| 133 | + .timeoutForEachRetry(timeoutForEachRetryMillis) |
| 134 | + .build() |
| 135 | + |
| 136 | + else -> |
| 137 | + throw IllegalArgumentException( |
| 138 | + "Unsupported oci.auth.type='$authType'. Supported: config, instance_principal, auto", |
| 139 | + ) |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + private fun createConfigAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { |
| 144 | + val profile = environment.getProperty("oci.config.profile", "DEFAULT").trim().ifEmpty { "DEFAULT" } |
| 145 | + val configPath = |
| 146 | + environment.getProperty("oci.config.path") |
| 147 | + ?.trim() |
| 148 | + ?.ifEmpty { null } |
| 149 | + ?.let { expandHome(it) } |
| 150 | + |
| 151 | + if (configPath == null) { |
| 152 | + return ConfigFileAuthenticationDetailsProvider(profile) |
| 153 | + } |
| 154 | + |
| 155 | + val configFile = ConfigFileReader.parse(configPath, profile) |
| 156 | + return ConfigFileAuthenticationDetailsProvider(configFile) |
| 157 | + } |
| 158 | + |
| 159 | + private fun expandHome(path: String): String { |
| 160 | + val home = System.getProperty("user.home") |
| 161 | + return if (path == "~") home else path.replaceFirst(Regex("^~(?=/|$)"), home) |
| 162 | + } |
| 163 | + |
| 164 | + private fun getSecretString( |
| 165 | + client: SecretsClient, |
| 166 | + secretId: String, |
| 167 | + ): String { |
| 168 | + val request = |
| 169 | + GetSecretBundleRequest.builder() |
| 170 | + .secretId(secretId) |
| 171 | + .build() |
| 172 | + val response = client.getSecretBundle(request) |
| 173 | + val content = (response.secretBundle.secretBundleContent as Base64SecretBundleContentDetails).content |
| 174 | + return String(Base64.getDecoder().decode(content)) |
| 175 | + } |
| 176 | +} |
0 commit comments