From 87a2846790ea86414bf4ca1e235dde8604c3f2f7 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 16:28:26 +0900 Subject: [PATCH 01/16] move to github packages and add oci vault support --- .github/workflows/publish-release.yml | 19 +++-- .github/workflows/publish-snapshot.yml | 19 +++-- build.gradle.kts | 17 ++-- settings.gradle.kts | 1 + .../build.gradle.kts | 11 +++ .../OciVaultEnvironmentPostProcessor.kt | 77 +++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + 7 files changed, 113 insertions(+), 32 deletions(-) create mode 100644 spring-boot-starter-waffle-oci-vault/build.gradle.kts create mode 100644 spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt create mode 100644 spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 67a5866..35a5712 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -4,6 +4,10 @@ on: release: types: [ published ] +permissions: + contents: write + packages: write + jobs: deploy: name: Publish @@ -11,22 +15,17 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 + uses: actions/checkout@v4 - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - name: Publish release + env: + GITHUB_TOKEN: ${{ github.token }} run: | ./gradlew clean publish -Pversion=${{ github.event.release.tag_name }} @@ -42,4 +41,4 @@ jobs: git checkout main git add ./gradle.properties git commit -m "Automated commit by GitHub Actions" - git push + git push \ No newline at end of file diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml index 844c752..98754e8 100644 --- a/.github/workflows/publish-snapshot.yml +++ b/.github/workflows/publish-snapshot.yml @@ -4,6 +4,10 @@ on: push: branches: [ main ] +permissions: + contents: read + packages: write + jobs: deploy: name: Publish @@ -11,21 +15,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 + uses: actions/checkout@v4 - name: Set up JDK 21 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '21' distribution: 'temurin' - name: Publish snapshot + env: + GITHUB_TOKEN: ${{ github.token }} run: | - ./gradlew clean publish + ./gradlew clean publish \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 25e900c..427ff41 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,9 +12,6 @@ plugins { id("maven-publish") } -java.sourceCompatibility = JavaVersion.VERSION_21 -java.targetCompatibility = JavaVersion.VERSION_21 - allprojects { repositories { mavenCentral() @@ -29,6 +26,8 @@ allprojects { } java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 withSourcesJar() withJavadocJar() } @@ -48,16 +47,10 @@ allprojects { publishing { repositories { maven { - val authToken = - properties["codeArtifactAuthToken"] as String? ?: ProcessBuilder( - "aws", "codeartifact", "get-authorization-token", - "--domain", "wafflestudio", "--domain-owner", "405906814034", - "--query", "authorizationToken", "--region", "ap-northeast-1", "--output", "text", - ).start().inputStream.bufferedReader().readText().trim() - url = uri("https://wafflestudio-405906814034.d.codeartifact.ap-northeast-1.amazonaws.com/maven/spring-waffle/") + url = uri("https://maven.pkg.github.com/wafflestudio/spring-waffle") credentials { - username = "aws" - password = authToken + username = "wafflestudio" + password = findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") ?: "" } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index d5675db..074f786 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,6 +3,7 @@ rootProject.name = "waffle-spring" include( "spring-boot-starter-waffle", "spring-boot-starter-waffle-secret-manager", + "spring-boot-starter-waffle-oci-vault", "truffle", "truffle:truffle-core", "truffle:truffle-logback", diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts new file mode 100644 index 0000000..d49c4c3 --- /dev/null +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -0,0 +1,11 @@ +group = "com.wafflestudio.spring" + +dependencies { + implementation("org.springframework.boot:spring-boot") + + implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1") + implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") + implementation("com.oracle.oci.sdk:oci-java-sdk-addons-oke-workload-identity:3.80.1") + + testImplementation(kotlin("test")) +} \ No newline at end of file diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt new file mode 100644 index 0000000..5debbcd --- /dev/null +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -0,0 +1,77 @@ +package com.wafflestudio.spring.ocivault.config + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.oracle.bmc.Region +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider +import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider +import com.oracle.bmc.secrets.SecretsClient +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest +import org.springframework.boot.EnvironmentPostProcessor +import org.springframework.boot.SpringApplication +import org.springframework.core.env.ConfigurableEnvironment +import org.springframework.core.env.MapPropertySource +import java.util.Base64 + +class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { + private val objectMapper = jacksonObjectMapper() + + override fun postProcessEnvironment( + environment: ConfigurableEnvironment, + application: SpringApplication, + ) { + val isAotProcessing = environment.getProperty("spring.aot.processing", Boolean::class.java, false) + if (isAotProcessing) { + return + } + val secretIdsProperty = environment.getProperty("oci-vault-secret-ids") ?: return + val secretIds = secretIdsProperty.split(",").map { it.trim() } + val region = Region.fromRegionId( + environment.getProperty("oci.vault.region", "ap-chuncheon-1"), + ) + + val authProvider = createAuthProvider(environment) + val client = SecretsClient.builder().region(region).build(authProvider) + val secrets = mutableMapOf() + + try { + secretIds.forEach { secretId -> + val secretString = getSecretString(client, secretId) + val parsedSecrets = objectMapper.readValue>(secretString) + secrets.putAll( + parsedSecrets.filterKeys { + environment.getProperty(it).isNullOrEmpty() + }, + ) + } + } finally { + client.close() + } + + if (secrets.isNotEmpty()) { + environment.propertySources.addFirst( + MapPropertySource("oci-vault-secrets", secrets), + ) + } + } + + private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { + val profiles = environment.activeProfiles.toSet() + return if (profiles.contains("dev") || profiles.contains("prod")) { + OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build() + } else { + ConfigFileAuthenticationDetailsProvider("DEFAULT") + } + } + + private fun getSecretString(client: SecretsClient, secretId: String): String { + val request = GetSecretBundleRequest.builder() + .secretId(secretId) + .build() + val response = client.getSecretBundle(request) + val content = (response.secretBundle.secretBundleContent as Base64SecretBundleContentDetails).content + return String(Base64.getDecoder().decode(content)) + } +} \ No newline at end of file diff --git a/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories b/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..a83c88c --- /dev/null +++ b/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.EnvironmentPostProcessor=com.wafflestudio.spring.ocivault.config.OciVaultEnvironmentPostProcessor \ No newline at end of file From 28d2bdf30d9a8d05e3c0faead10796580fecde51 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 16:35:02 +0900 Subject: [PATCH 02/16] fix ktlint --- .../build.gradle.kts | 2 +- .../OciVaultEnvironmentPostProcessor.kt | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts index d49c4c3..8aeca48 100644 --- a/spring-boot-starter-waffle-oci-vault/build.gradle.kts +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -8,4 +8,4 @@ dependencies { implementation("com.oracle.oci.sdk:oci-java-sdk-addons-oke-workload-identity:3.80.1") testImplementation(kotlin("test")) -} \ No newline at end of file +} diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 5debbcd..b4a8353 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -28,9 +28,10 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } val secretIdsProperty = environment.getProperty("oci-vault-secret-ids") ?: return val secretIds = secretIdsProperty.split(",").map { it.trim() } - val region = Region.fromRegionId( - environment.getProperty("oci.vault.region", "ap-chuncheon-1"), - ) + val region = + Region.fromRegionId( + environment.getProperty("oci.vault.region", "ap-chuncheon-1"), + ) val authProvider = createAuthProvider(environment) val client = SecretsClient.builder().region(region).build(authProvider) @@ -66,12 +67,16 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } } - private fun getSecretString(client: SecretsClient, secretId: String): String { - val request = GetSecretBundleRequest.builder() - .secretId(secretId) - .build() + private fun getSecretString( + client: SecretsClient, + secretId: String, + ): String { + val request = + GetSecretBundleRequest.builder() + .secretId(secretId) + .build() val response = client.getSecretBundle(request) val content = (response.secretBundle.secretBundleContent as Base64SecretBundleContentDetails).content return String(Base64.getDecoder().decode(content)) } -} \ No newline at end of file +} From 58b99d4df6ec147b135103e37318ffa7a83d1123 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Thu, 12 Feb 2026 00:22:33 +0900 Subject: [PATCH 03/16] use api key only --- .../ocivault/config/OciVaultEnvironmentPostProcessor.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index b4a8353..6dd30a7 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -5,7 +5,6 @@ import com.fasterxml.jackson.module.kotlin.readValue import com.oracle.bmc.Region import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider -import com.oracle.bmc.auth.okeworkloadidentity.OkeWorkloadIdentityAuthenticationDetailsProvider import com.oracle.bmc.secrets.SecretsClient import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails import com.oracle.bmc.secrets.requests.GetSecretBundleRequest @@ -59,12 +58,7 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { - val profiles = environment.activeProfiles.toSet() - return if (profiles.contains("dev") || profiles.contains("prod")) { - OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build() - } else { - ConfigFileAuthenticationDetailsProvider("DEFAULT") - } + return ConfigFileAuthenticationDetailsProvider("DEFAULT") } private fun getSecretString( From be56a6fde597089fa6d2e932343cbaf5bc4ed57a Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Thu, 12 Feb 2026 14:07:31 +0900 Subject: [PATCH 04/16] spring boot 3.x compatibility --- .../spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt | 2 +- .../src/main/resources/META-INF/spring.factories | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 6dd30a7..8505168 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -8,8 +8,8 @@ import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider import com.oracle.bmc.secrets.SecretsClient import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails import com.oracle.bmc.secrets.requests.GetSecretBundleRequest -import org.springframework.boot.EnvironmentPostProcessor import org.springframework.boot.SpringApplication +import org.springframework.boot.env.EnvironmentPostProcessor import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource import java.util.Base64 diff --git a/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories b/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories index a83c88c..0476b8d 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories +++ b/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.EnvironmentPostProcessor=com.wafflestudio.spring.ocivault.config.OciVaultEnvironmentPostProcessor \ No newline at end of file +org.springframework.boot.env.EnvironmentPostProcessor=com.wafflestudio.spring.ocivault.config.OciVaultEnvironmentPostProcessor \ No newline at end of file From c8cd0afa02988db2339f95df4104b71a61fa8286 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Thu, 12 Feb 2026 14:47:32 +0900 Subject: [PATCH 05/16] exclude hk2 --- spring-boot-starter-waffle-oci-vault/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts index 8aeca48..accefb6 100644 --- a/spring-boot-starter-waffle-oci-vault/build.gradle.kts +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -4,7 +4,9 @@ dependencies { implementation("org.springframework.boot:spring-boot") implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1") - implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") + implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") { + exclude(group = "org.glassfish.jersey.inject", module = "jersey-hk2") + } implementation("com.oracle.oci.sdk:oci-java-sdk-addons-oke-workload-identity:3.80.1") testImplementation(kotlin("test")) From fd6ba5d95d083cd5602365a484253614e27afe13 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Thu, 12 Feb 2026 15:11:47 +0900 Subject: [PATCH 06/16] remove oci sdk deps --- .../build.gradle.kts | 6 - .../OciVaultEnvironmentPostProcessor.kt | 145 ++++++++++++++---- 2 files changed, 112 insertions(+), 39 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts index accefb6..75dd58e 100644 --- a/spring-boot-starter-waffle-oci-vault/build.gradle.kts +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -3,11 +3,5 @@ group = "com.wafflestudio.spring" dependencies { implementation("org.springframework.boot:spring-boot") - implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1") - implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") { - exclude(group = "org.glassfish.jersey.inject", module = "jersey-hk2") - } - implementation("com.oracle.oci.sdk:oci-java-sdk-addons-oke-workload-identity:3.80.1") - testImplementation(kotlin("test")) } diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 8505168..8895895 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -2,20 +2,27 @@ package com.wafflestudio.spring.ocivault.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue -import com.oracle.bmc.Region -import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider -import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider -import com.oracle.bmc.secrets.SecretsClient -import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails -import com.oracle.bmc.secrets.requests.GetSecretBundleRequest import org.springframework.boot.SpringApplication import org.springframework.boot.env.EnvironmentPostProcessor import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.PKCS8EncodedKeySpec +import java.time.ZoneOffset +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter import java.util.Base64 class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { private val objectMapper = jacksonObjectMapper() + private val httpClient = HttpClient.newHttpClient() override fun postProcessEnvironment( environment: ConfigurableEnvironment, @@ -27,27 +34,19 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } val secretIdsProperty = environment.getProperty("oci-vault-secret-ids") ?: return val secretIds = secretIdsProperty.split(",").map { it.trim() } - val region = - Region.fromRegionId( - environment.getProperty("oci.vault.region", "ap-chuncheon-1"), - ) + val region = environment.getProperty("oci.vault.region", "ap-chuncheon-1") - val authProvider = createAuthProvider(environment) - val client = SecretsClient.builder().region(region).build(authProvider) + val ociConfig = loadOciConfig() val secrets = mutableMapOf() - try { - secretIds.forEach { secretId -> - val secretString = getSecretString(client, secretId) - val parsedSecrets = objectMapper.readValue>(secretString) - secrets.putAll( - parsedSecrets.filterKeys { - environment.getProperty(it).isNullOrEmpty() - }, - ) - } - } finally { - client.close() + secretIds.forEach { secretId -> + val secretString = getSecretString(ociConfig, region, secretId) + val parsedSecrets = objectMapper.readValue>(secretString) + secrets.putAll( + parsedSecrets.filterKeys { + environment.getProperty(it).isNullOrEmpty() + }, + ) } if (secrets.isNotEmpty()) { @@ -57,20 +56,100 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } } - private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { - return ConfigFileAuthenticationDetailsProvider("DEFAULT") - } - private fun getSecretString( - client: SecretsClient, + config: OciConfig, + region: String, secretId: String, ): String { + val host = "secrets.vaults.$region.oci.oraclecloud.com" + val path = "/20190301/secretbundles/$secretId" + val date = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)) + + val signingString = "(request-target): get $path\ndate: $date\nhost: $host" + val signature = sign(config.privateKey, signingString) + val keyId = "${config.tenancy}/${config.user}/${config.fingerprint}" + val authHeader = + """Signature version="1",keyId="$keyId",algorithm="rsa-sha256",headers="(request-target) date host",signature="$signature"""" + val request = - GetSecretBundleRequest.builder() - .secretId(secretId) + HttpRequest.newBuilder() + .uri(URI.create("https://$host$path")) + .header("date", date) + .header("authorization", authHeader) + .GET() .build() - val response = client.getSecretBundle(request) - val content = (response.secretBundle.secretBundleContent as Base64SecretBundleContentDetails).content + + val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() != 200) { + throw RuntimeException("OCI Vault API error (${response.statusCode()}): ${response.body()}") + } + + val body = objectMapper.readValue>(response.body()) + + @Suppress("UNCHECKED_CAST") + val secretBundleContent = + body["secretBundleContent"] as Map + val content = secretBundleContent["content"] as String return String(Base64.getDecoder().decode(content)) } + + private fun sign( + privateKeyPem: String, + signingString: String, + ): String { + val pemContent = + privateKeyPem + .substringAfter("-----\n") + .substringBefore("\n-----") + .replace("\\s".toRegex(), "") + val keyBytes = Base64.getDecoder().decode(pemContent) + val keySpec = PKCS8EncodedKeySpec(keyBytes) + val keyFactory = KeyFactory.getInstance("RSA") + val key = keyFactory.generatePrivate(keySpec) + val signature = Signature.getInstance("SHA256withRSA") + signature.initSign(key) + signature.update(signingString.toByteArray()) + return Base64.getEncoder().encodeToString(signature.sign()) + } + + private fun loadOciConfig(): OciConfig { + val configPath = Path.of(System.getProperty("user.home"), ".oci", "config") + val lines = Files.readAllLines(configPath) + val props = mutableMapOf() + var inDefaultProfile = false + + for (line in lines) { + val trimmed = line.trim() + if (trimmed == "[DEFAULT]") { + inDefaultProfile = true + continue + } + if (trimmed.startsWith("[")) { + inDefaultProfile = false + continue + } + if (inDefaultProfile && trimmed.contains("=")) { + val (key, value) = trimmed.split("=", limit = 2) + props[key.trim()] = value.trim() + } + } + + val keyFilePath = + props["key_file"]?.replace("~", System.getProperty("user.home")) + ?: throw RuntimeException("key_file not found in OCI config") + + return OciConfig( + tenancy = props["tenancy"] ?: throw RuntimeException("tenancy not found in OCI config"), + user = props["user"] ?: throw RuntimeException("user not found in OCI config"), + fingerprint = props["fingerprint"] ?: throw RuntimeException("fingerprint not found in OCI config"), + privateKey = Files.readString(Path.of(keyFilePath)), + ) + } + + private data class OciConfig( + val tenancy: String, + val user: String, + val fingerprint: String, + val privateKey: String, + ) } From aedcadf73d623dca167704e54c46383c09b8277a Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Thu, 12 Feb 2026 15:32:38 +0900 Subject: [PATCH 07/16] revert spring boot 3.x compatibility --- .../spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt | 2 +- .../src/main/resources/META-INF/spring.factories | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 8895895..14a3405 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -2,8 +2,8 @@ package com.wafflestudio.spring.ocivault.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import org.springframework.boot.EnvironmentPostProcessor import org.springframework.boot.SpringApplication -import org.springframework.boot.env.EnvironmentPostProcessor import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource import java.net.URI diff --git a/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories b/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories index 0476b8d..a83c88c 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories +++ b/spring-boot-starter-waffle-oci-vault/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.env.EnvironmentPostProcessor=com.wafflestudio.spring.ocivault.config.OciVaultEnvironmentPostProcessor \ No newline at end of file +org.springframework.boot.EnvironmentPostProcessor=com.wafflestudio.spring.ocivault.config.OciVaultEnvironmentPostProcessor \ No newline at end of file From 8a4a10ad5b40ec2ab9c4abb5ee07f855df7ae641 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 10:39:30 +0900 Subject: [PATCH 08/16] rewrite with oci sdk --- .../README.md | 27 +++ .../build.gradle.kts | 7 + .../OciVaultEnvironmentPostProcessor.kt | 194 ++++++++---------- 3 files changed, 118 insertions(+), 110 deletions(-) create mode 100644 spring-boot-starter-waffle-oci-vault/README.md diff --git a/spring-boot-starter-waffle-oci-vault/README.md b/spring-boot-starter-waffle-oci-vault/README.md new file mode 100644 index 0000000..e2bc1ca --- /dev/null +++ b/spring-boot-starter-waffle-oci-vault/README.md @@ -0,0 +1,27 @@ +# spring-boot-starter-waffle-oci-vault + +Loads OCI Vault Secrets into Spring `Environment` at startup (as an `EnvironmentPostProcessor`). + +## Properties + +Required: +- `oci.vault.secret-ids`: Comma-separated secret OCIDs (`ocid1.vaultsecret...`). +- `oci.vault.region`: Region id (default: `ap-chuncheon-1`). + +Auth: +- `oci.auth.type`: `auto` (default), `instance_principal`, or `config`. + - `config`: Uses OCI config file credentials. + - `instance_principal`: Uses Instance Principal (Dynamic Group) credentials. + - `auto`: Tries Instance Principal first; if it fails, falls back to config file credentials. + +Config-file auth options: +- `oci.config.path`: Path to OCI config file (default: `~/.oci/config`). +- `oci.config.profile`: Profile name (default: `DEFAULT`). + +## Secret Format + +Each secret is expected to be a JSON object. Keys from the JSON are added as properties only if they are not already set in the environment. + +## Notes + +- OKE Workload Identity is an OKE feature and not implemented by this starter at the moment. If you run on OKE Basic and you cannot use instance principals from pods, use `oci.auth.type=config` (or `auto`) with a mounted `~/.oci/config` + key. diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts index 75dd58e..3afabb5 100644 --- a/spring-boot-starter-waffle-oci-vault/build.gradle.kts +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -3,5 +3,12 @@ group = "com.wafflestudio.spring" dependencies { implementation("org.springframework.boot:spring-boot") + // OCI Vault (Secrets) access via OCI Java SDK. + implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1") + implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") { + // Avoid pulling HK2 injection implementation transitively. + exclude(group = "org.glassfish.jersey.inject", module = "jersey-hk2") + } + testImplementation(kotlin("test")) } diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 14a3405..ae5e045 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -2,27 +2,24 @@ package com.wafflestudio.spring.ocivault.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import com.oracle.bmc.ConfigFileReader +import com.oracle.bmc.Region +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider +import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider +import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider +import com.oracle.bmc.secrets.SecretsClient +import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails +import com.oracle.bmc.secrets.requests.GetSecretBundleRequest import org.springframework.boot.EnvironmentPostProcessor import org.springframework.boot.SpringApplication import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse -import java.nio.file.Files -import java.nio.file.Path -import java.security.KeyFactory -import java.security.Signature -import java.security.spec.PKCS8EncodedKeySpec -import java.time.ZoneOffset -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter +import org.slf4j.LoggerFactory import java.util.Base64 class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { + private val log = LoggerFactory.getLogger(javaClass) private val objectMapper = jacksonObjectMapper() - private val httpClient = HttpClient.newHttpClient() override fun postProcessEnvironment( environment: ConfigurableEnvironment, @@ -32,21 +29,29 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { if (isAotProcessing) { return } - val secretIdsProperty = environment.getProperty("oci-vault-secret-ids") ?: return + val secretIdsProperty = environment.getProperty("oci.vault.secret-ids") ?: return val secretIds = secretIdsProperty.split(",").map { it.trim() } - val region = environment.getProperty("oci.vault.region", "ap-chuncheon-1") + val region = + Region.fromRegionId( + environment.getProperty("oci.vault.region", "ap-chuncheon-1"), + ) - val ociConfig = loadOciConfig() + val authProvider = createAuthProvider(environment) + val client = SecretsClient.builder().region(region).build(authProvider) val secrets = mutableMapOf() - secretIds.forEach { secretId -> - val secretString = getSecretString(ociConfig, region, secretId) - val parsedSecrets = objectMapper.readValue>(secretString) - secrets.putAll( - parsedSecrets.filterKeys { - environment.getProperty(it).isNullOrEmpty() - }, - ) + try { + secretIds.forEach { secretId -> + val secretString = getSecretString(client, secretId) + val parsedSecrets = objectMapper.readValue>(secretString) + secrets.putAll( + parsedSecrets.filterKeys { + environment.getProperty(it).isNullOrEmpty() + }, + ) + } + } finally { + client.close() } if (secrets.isNotEmpty()) { @@ -56,100 +61,69 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } } - private fun getSecretString( - config: OciConfig, - region: String, - secretId: String, - ): String { - val host = "secrets.vaults.$region.oci.oraclecloud.com" - val path = "/20190301/secretbundles/$secretId" - val date = DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC)) - - val signingString = "(request-target): get $path\ndate: $date\nhost: $host" - val signature = sign(config.privateKey, signingString) - val keyId = "${config.tenancy}/${config.user}/${config.fingerprint}" - val authHeader = - """Signature version="1",keyId="$keyId",algorithm="rsa-sha256",headers="(request-target) date host",signature="$signature"""" - - val request = - HttpRequest.newBuilder() - .uri(URI.create("https://$host$path")) - .header("date", date) - .header("authorization", authHeader) - .GET() - .build() + private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { + // Default to `auto` so apps "just work" on OCI (Instance Principals) and locally (config file fallback). + val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase() + return when (authType) { + "auto" -> { + try { + InstancePrincipalsAuthenticationDetailsProvider.builder().build() + } catch (e: Exception) { + log.info("OCI instance principal auth failed; falling back to config file auth (oci.auth.type=auto).", e) + createConfigAuthProvider(environment) + } + } - val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) - if (response.statusCode() != 200) { - throw RuntimeException("OCI Vault API error (${response.statusCode()}): ${response.body()}") + "config", + "configfile", + "config_file", + "config-file", + -> createConfigAuthProvider(environment) + + "instance_principal", + "instanceprincipal", + "instance-principal", + "ip", + -> InstancePrincipalsAuthenticationDetailsProvider.builder().build() + + else -> + throw IllegalArgumentException( + "Unsupported oci.auth.type='$authType'. Supported: config, instance_principal, auto", + ) } - - val body = objectMapper.readValue>(response.body()) - - @Suppress("UNCHECKED_CAST") - val secretBundleContent = - body["secretBundleContent"] as Map - val content = secretBundleContent["content"] as String - return String(Base64.getDecoder().decode(content)) } - private fun sign( - privateKeyPem: String, - signingString: String, - ): String { - val pemContent = - privateKeyPem - .substringAfter("-----\n") - .substringBefore("\n-----") - .replace("\\s".toRegex(), "") - val keyBytes = Base64.getDecoder().decode(pemContent) - val keySpec = PKCS8EncodedKeySpec(keyBytes) - val keyFactory = KeyFactory.getInstance("RSA") - val key = keyFactory.generatePrivate(keySpec) - val signature = Signature.getInstance("SHA256withRSA") - signature.initSign(key) - signature.update(signingString.toByteArray()) - return Base64.getEncoder().encodeToString(signature.sign()) - } + private fun createConfigAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { + val profile = environment.getProperty("oci.config.profile", "DEFAULT").trim().ifEmpty { "DEFAULT" } + val configPath = + environment.getProperty("oci.config.path") + ?.trim() + ?.ifEmpty { null } + ?.let { expandHome(it) } - private fun loadOciConfig(): OciConfig { - val configPath = Path.of(System.getProperty("user.home"), ".oci", "config") - val lines = Files.readAllLines(configPath) - val props = mutableMapOf() - var inDefaultProfile = false - - for (line in lines) { - val trimmed = line.trim() - if (trimmed == "[DEFAULT]") { - inDefaultProfile = true - continue - } - if (trimmed.startsWith("[")) { - inDefaultProfile = false - continue - } - if (inDefaultProfile && trimmed.contains("=")) { - val (key, value) = trimmed.split("=", limit = 2) - props[key.trim()] = value.trim() - } + if (configPath == null) { + return ConfigFileAuthenticationDetailsProvider(profile) } - val keyFilePath = - props["key_file"]?.replace("~", System.getProperty("user.home")) - ?: throw RuntimeException("key_file not found in OCI config") + val configFile = ConfigFileReader.parse(configPath, profile) + return ConfigFileAuthenticationDetailsProvider(configFile) + } - return OciConfig( - tenancy = props["tenancy"] ?: throw RuntimeException("tenancy not found in OCI config"), - user = props["user"] ?: throw RuntimeException("user not found in OCI config"), - fingerprint = props["fingerprint"] ?: throw RuntimeException("fingerprint not found in OCI config"), - privateKey = Files.readString(Path.of(keyFilePath)), - ) + private fun expandHome(path: String): String { + val home = System.getProperty("user.home") + return if (path == "~") home else path.replaceFirst(Regex("^~(?=/|$)"), home) } - private data class OciConfig( - val tenancy: String, - val user: String, - val fingerprint: String, - val privateKey: String, - ) + private fun getSecretString( + client: SecretsClient, + secretId: String, + ): String { + val request = + GetSecretBundleRequest.builder() + .secretId(secretId) + .build() + val response = client.getSecretBundle(request) + val content = (response.secretBundle.secretBundleContent as Base64SecretBundleContentDetails).content + return String(Base64.getDecoder().decode(content)) + } } From 7c5cd5a166644d7098a0efc60819879ba4146929 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 11:40:09 +0900 Subject: [PATCH 09/16] use getProperty kotlin extension --- .../spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index ae5e045..cbece74 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -15,6 +15,7 @@ import org.springframework.boot.SpringApplication import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource import org.slf4j.LoggerFactory +import org.springframework.core.env.getProperty import java.util.Base64 class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { @@ -25,7 +26,7 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { environment: ConfigurableEnvironment, application: SpringApplication, ) { - val isAotProcessing = environment.getProperty("spring.aot.processing", Boolean::class.java, false) + val isAotProcessing = environment.getProperty("spring.aot.processing", false) if (isAotProcessing) { return } From 457b0cef268a9dbc9977b394b7259cebba8eeb07 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 11:40:33 +0900 Subject: [PATCH 10/16] refactor --- .../ocivault/config/OciVaultEnvironmentPostProcessor.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index cbece74..8055b68 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -41,7 +41,7 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { val client = SecretsClient.builder().region(region).build(authProvider) val secrets = mutableMapOf() - try { + client.use { client -> secretIds.forEach { secretId -> val secretString = getSecretString(client, secretId) val parsedSecrets = objectMapper.readValue>(secretString) @@ -51,8 +51,6 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { }, ) } - } finally { - client.close() } if (secrets.isNotEmpty()) { @@ -64,8 +62,7 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { // Default to `auto` so apps "just work" on OCI (Instance Principals) and locally (config file fallback). - val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase() - return when (authType) { + return when (val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase()) { "auto" -> { try { InstancePrincipalsAuthenticationDetailsProvider.builder().build() From f24c63d10f8bf69a023201b3a2858cce16ea83bf Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 12:33:26 +0900 Subject: [PATCH 11/16] try config file first --- spring-boot-starter-waffle-oci-vault/README.md | 2 +- .../ocivault/config/OciVaultEnvironmentPostProcessor.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/README.md b/spring-boot-starter-waffle-oci-vault/README.md index e2bc1ca..18270f5 100644 --- a/spring-boot-starter-waffle-oci-vault/README.md +++ b/spring-boot-starter-waffle-oci-vault/README.md @@ -12,7 +12,7 @@ Auth: - `oci.auth.type`: `auto` (default), `instance_principal`, or `config`. - `config`: Uses OCI config file credentials. - `instance_principal`: Uses Instance Principal (Dynamic Group) credentials. - - `auto`: Tries Instance Principal first; if it fails, falls back to config file credentials. + - `auto`: Tries config file credentials first; if it fails, falls back to Instance Principal. Config-file auth options: - `oci.config.path`: Path to OCI config file (default: `~/.oci/config`). diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 8055b68..a07d8cd 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -61,14 +61,14 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { - // Default to `auto` so apps "just work" on OCI (Instance Principals) and locally (config file fallback). + // Default to `auto` so apps "just work" locally (config file) and on OCI (Instance Principals fallback). return when (val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase()) { "auto" -> { try { - InstancePrincipalsAuthenticationDetailsProvider.builder().build() - } catch (e: Exception) { - log.info("OCI instance principal auth failed; falling back to config file auth (oci.auth.type=auto).", e) createConfigAuthProvider(environment) + } catch (e: Exception) { + log.info("OCI config file auth failed; falling back to instance principal auth (oci.auth.type=auto).", e) + InstancePrincipalsAuthenticationDetailsProvider.builder().build() } } From af7ece2ecdb0d032993273e35b10fce70b9cf99c Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 14:38:22 +0900 Subject: [PATCH 12/16] set instanceprincipal timeout --- .../OciVaultEnvironmentPostProcessor.kt | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index a07d8cd..4abd873 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -2,26 +2,32 @@ package com.wafflestudio.spring.ocivault.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import com.oracle.bmc.ClientConfiguration import com.oracle.bmc.ConfigFileReader import com.oracle.bmc.Region import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider +import com.oracle.bmc.http.ClientConfigurator +import com.oracle.bmc.http.client.StandardClientProperties import com.oracle.bmc.secrets.SecretsClient import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails import com.oracle.bmc.secrets.requests.GetSecretBundleRequest +import org.slf4j.LoggerFactory import org.springframework.boot.EnvironmentPostProcessor import org.springframework.boot.SpringApplication import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.MapPropertySource -import org.slf4j.LoggerFactory import org.springframework.core.env.getProperty +import java.time.Duration import java.util.Base64 class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { private val log = LoggerFactory.getLogger(javaClass) private val objectMapper = jacksonObjectMapper() + private val ociTimeout: Duration = Duration.ofSeconds(10) + override fun postProcessEnvironment( environment: ConfigurableEnvironment, application: SpringApplication, @@ -38,7 +44,18 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { ) val authProvider = createAuthProvider(environment) - val client = SecretsClient.builder().region(region).build(authProvider) + val client = + SecretsClient + .builder() + .configuration( + ClientConfiguration + .builder() + .connectionTimeoutMillis(ociTimeout.toMillis().toInt()) + .readTimeoutMillis(ociTimeout.toMillis().toInt()) + .build(), + ) + .region(region) + .build(authProvider) val secrets = mutableMapOf() client.use { client -> @@ -61,6 +78,12 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { } private fun createAuthProvider(environment: ConfigurableEnvironment): BasicAuthenticationDetailsProvider { + val instancePrincipalTimeoutConfigurator = + ClientConfigurator { builder -> + builder.property(StandardClientProperties.CONNECT_TIMEOUT, ociTimeout) + builder.property(StandardClientProperties.READ_TIMEOUT, ociTimeout) + } + // Default to `auto` so apps "just work" locally (config file) and on OCI (Instance Principals fallback). return when (val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase()) { "auto" -> { @@ -68,7 +91,11 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { createConfigAuthProvider(environment) } catch (e: Exception) { log.info("OCI config file auth failed; falling back to instance principal auth (oci.auth.type=auto).", e) - InstancePrincipalsAuthenticationDetailsProvider.builder().build() + InstancePrincipalsAuthenticationDetailsProvider + .builder() + .federationClientConfigurator(instancePrincipalTimeoutConfigurator) + .timeoutForEachRetry(ociTimeout.toMillis().toInt()) + .build() } } @@ -82,7 +109,12 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { "instanceprincipal", "instance-principal", "ip", - -> InstancePrincipalsAuthenticationDetailsProvider.builder().build() + -> + InstancePrincipalsAuthenticationDetailsProvider + .builder() + .federationClientConfigurator(instancePrincipalTimeoutConfigurator) + .timeoutForEachRetry(ociTimeout.toMillis().toInt()) + .build() else -> throw IllegalArgumentException( From 586e413e06c45afdfe1813b0369aa2014cc52274 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 14:58:38 +0900 Subject: [PATCH 13/16] do not exclude hk2 --- spring-boot-starter-waffle-oci-vault/build.gradle.kts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts index 3afabb5..43b2423 100644 --- a/spring-boot-starter-waffle-oci-vault/build.gradle.kts +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -5,10 +5,7 @@ dependencies { // OCI Vault (Secrets) access via OCI Java SDK. implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1") - implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") { - // Avoid pulling HK2 injection implementation transitively. - exclude(group = "org.glassfish.jersey.inject", module = "jersey-hk2") - } + implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") testImplementation(kotlin("test")) } From c0e4e2cd12bb32e6e78fd4252918dcec417924f6 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 15:34:01 +0900 Subject: [PATCH 14/16] set retry count --- .../OciVaultEnvironmentPostProcessor.kt | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 4abd873..4cd6be9 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -13,6 +13,9 @@ import com.oracle.bmc.http.client.StandardClientProperties import com.oracle.bmc.secrets.SecretsClient import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails import com.oracle.bmc.secrets.requests.GetSecretBundleRequest +import com.oracle.bmc.retrier.RetryConfiguration +import com.oracle.bmc.waiter.FixedTimeDelayStrategy +import com.oracle.bmc.waiter.MaxAttemptsTerminationStrategy import org.slf4j.LoggerFactory import org.springframework.boot.EnvironmentPostProcessor import org.springframework.boot.SpringApplication @@ -43,6 +46,14 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { environment.getProperty("oci.vault.region", "ap-chuncheon-1"), ) + val maxAttempts = environment.getProperty("oci.retry.max-attempts", 2).coerceAtLeast(1) + val retryDelayMillis = environment.getProperty("oci.retry.delay-millis", 0).coerceAtLeast(0).toLong() + val retryConfiguration = + RetryConfiguration.builder() + .terminationStrategy(MaxAttemptsTerminationStrategy(maxAttempts)) + .delayStrategy(FixedTimeDelayStrategy(retryDelayMillis)) + .build() + val authProvider = createAuthProvider(environment) val client = SecretsClient @@ -52,6 +63,7 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { .builder() .connectionTimeoutMillis(ociTimeout.toMillis().toInt()) .readTimeoutMillis(ociTimeout.toMillis().toInt()) + .retryConfiguration(retryConfiguration) .build(), ) .region(region) @@ -84,7 +96,10 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { builder.property(StandardClientProperties.READ_TIMEOUT, ociTimeout) } - // Default to `auto` so apps "just work" locally (config file) and on OCI (Instance Principals fallback). + val timeoutForEachRetryMillis = + environment.getProperty("oci.auth.timeout-for-each-retry-millis", ociTimeout.toMillis().toInt()) + val detectEndpointRetries = environment.getProperty("oci.auth.detect-endpoint-retries", 1) + return when (val authType = environment.getProperty("oci.auth.type", "auto").trim().lowercase()) { "auto" -> { try { @@ -94,7 +109,8 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { InstancePrincipalsAuthenticationDetailsProvider .builder() .federationClientConfigurator(instancePrincipalTimeoutConfigurator) - .timeoutForEachRetry(ociTimeout.toMillis().toInt()) + .detectEndpointRetries(detectEndpointRetries) + .timeoutForEachRetry(timeoutForEachRetryMillis) .build() } } @@ -113,7 +129,8 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { InstancePrincipalsAuthenticationDetailsProvider .builder() .federationClientConfigurator(instancePrincipalTimeoutConfigurator) - .timeoutForEachRetry(ociTimeout.toMillis().toInt()) + .detectEndpointRetries(detectEndpointRetries) + .timeoutForEachRetry(timeoutForEachRetryMillis) .build() else -> From ba3d57a909a145fd89c29cd99b67de87f8325683 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 17:57:38 +0900 Subject: [PATCH 15/16] fix lint --- .../ocivault/config/OciVaultEnvironmentPostProcessor.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt index 4cd6be9..d5c171a 100644 --- a/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt +++ b/spring-boot-starter-waffle-oci-vault/src/main/kotlin/com/wafflestudio/spring/ocivault/config/OciVaultEnvironmentPostProcessor.kt @@ -10,10 +10,10 @@ import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider import com.oracle.bmc.http.ClientConfigurator import com.oracle.bmc.http.client.StandardClientProperties +import com.oracle.bmc.retrier.RetryConfiguration import com.oracle.bmc.secrets.SecretsClient import com.oracle.bmc.secrets.model.Base64SecretBundleContentDetails import com.oracle.bmc.secrets.requests.GetSecretBundleRequest -import com.oracle.bmc.retrier.RetryConfiguration import com.oracle.bmc.waiter.FixedTimeDelayStrategy import com.oracle.bmc.waiter.MaxAttemptsTerminationStrategy import org.slf4j.LoggerFactory @@ -129,8 +129,8 @@ class OciVaultEnvironmentPostProcessor : EnvironmentPostProcessor { InstancePrincipalsAuthenticationDetailsProvider .builder() .federationClientConfigurator(instancePrincipalTimeoutConfigurator) - .detectEndpointRetries(detectEndpointRetries) - .timeoutForEachRetry(timeoutForEachRetryMillis) + .detectEndpointRetries(detectEndpointRetries) + .timeoutForEachRetry(timeoutForEachRetryMillis) .build() else -> From d7aa139bf6fbd6b7e652a8cc5c0c3c3898c37988 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 18:27:20 +0900 Subject: [PATCH 16/16] =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring-boot-starter-waffle-oci-vault/README.md | 2 +- spring-boot-starter-waffle-oci-vault/build.gradle.kts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-boot-starter-waffle-oci-vault/README.md b/spring-boot-starter-waffle-oci-vault/README.md index 18270f5..f7d8340 100644 --- a/spring-boot-starter-waffle-oci-vault/README.md +++ b/spring-boot-starter-waffle-oci-vault/README.md @@ -24,4 +24,4 @@ Each secret is expected to be a JSON object. Keys from the JSON are added as pro ## Notes -- OKE Workload Identity is an OKE feature and not implemented by this starter at the moment. If you run on OKE Basic and you cannot use instance principals from pods, use `oci.auth.type=config` (or `auto`) with a mounted `~/.oci/config` + key. +- OKE Workload Identity is an Enhanced cluster feature and not implemented by this starter at the moment. diff --git a/spring-boot-starter-waffle-oci-vault/build.gradle.kts b/spring-boot-starter-waffle-oci-vault/build.gradle.kts index 43b2423..59ce227 100644 --- a/spring-boot-starter-waffle-oci-vault/build.gradle.kts +++ b/spring-boot-starter-waffle-oci-vault/build.gradle.kts @@ -3,7 +3,6 @@ group = "com.wafflestudio.spring" dependencies { implementation("org.springframework.boot:spring-boot") - // OCI Vault (Secrets) access via OCI Java SDK. implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1") implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1")