Skip to content

Commit 43d3e0e

Browse files
authored
Move to github packages and add oci vault support (#15)
* move to github packages and add oci vault support * fix ktlint * use api key only * spring boot 3.x compatibility * exclude hk2 * remove oci sdk deps * revert spring boot 3.x compatibility * rewrite with oci sdk * use getProperty kotlin extension * refactor * try config file first * set instanceprincipal timeout * do not exclude hk2 * set retry count * fix lint * 설명 수정
1 parent 100fef5 commit 43d3e0e

8 files changed

Lines changed: 238 additions & 32 deletions

File tree

.github/workflows/publish-release.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,28 @@ on:
44
release:
55
types: [ published ]
66

7+
permissions:
8+
contents: write
9+
packages: write
10+
711
jobs:
812
deploy:
913
name: Publish
1014
runs-on: ubuntu-latest
1115

1216
steps:
1317
- name: Checkout
14-
uses: actions/checkout@v2
15-
16-
- name: Configure AWS credentials
17-
uses: aws-actions/configure-aws-credentials@v1
18-
with:
19-
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
20-
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
21-
aws-region: ap-northeast-2
18+
uses: actions/checkout@v4
2219

2320
- name: Set up JDK 21
24-
uses: actions/setup-java@v3
21+
uses: actions/setup-java@v4
2522
with:
2623
java-version: '21'
2724
distribution: 'temurin'
2825

2926
- name: Publish release
27+
env:
28+
GITHUB_TOKEN: ${{ github.token }}
3029
run: |
3130
./gradlew clean publish -Pversion=${{ github.event.release.tag_name }}
3231
@@ -42,4 +41,4 @@ jobs:
4241
git checkout main
4342
git add ./gradle.properties
4443
git commit -m "Automated commit by GitHub Actions"
45-
git push
44+
git push

.github/workflows/publish-snapshot.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,27 @@ on:
44
push:
55
branches: [ main ]
66

7+
permissions:
8+
contents: read
9+
packages: write
10+
711
jobs:
812
deploy:
913
name: Publish
1014
runs-on: ubuntu-latest
1115

1216
steps:
1317
- name: Checkout
14-
uses: actions/checkout@v2
15-
16-
- name: Configure AWS credentials
17-
uses: aws-actions/configure-aws-credentials@v1
18-
with:
19-
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
20-
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
21-
aws-region: ap-northeast-2
18+
uses: actions/checkout@v4
2219

2320
- name: Set up JDK 21
24-
uses: actions/setup-java@v3
21+
uses: actions/setup-java@v4
2522
with:
2623
java-version: '21'
2724
distribution: 'temurin'
2825

2926
- name: Publish snapshot
27+
env:
28+
GITHUB_TOKEN: ${{ github.token }}
3029
run: |
31-
./gradlew clean publish
30+
./gradlew clean publish

build.gradle.kts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ plugins {
1212
id("maven-publish")
1313
}
1414

15-
java.sourceCompatibility = JavaVersion.VERSION_21
16-
java.targetCompatibility = JavaVersion.VERSION_21
17-
1815
allprojects {
1916
repositories {
2017
mavenCentral()
@@ -29,6 +26,8 @@ allprojects {
2926
}
3027

3128
java {
29+
sourceCompatibility = JavaVersion.VERSION_21
30+
targetCompatibility = JavaVersion.VERSION_21
3231
withSourcesJar()
3332
withJavadocJar()
3433
}
@@ -48,16 +47,10 @@ allprojects {
4847
publishing {
4948
repositories {
5049
maven {
51-
val authToken =
52-
properties["codeArtifactAuthToken"] as String? ?: ProcessBuilder(
53-
"aws", "codeartifact", "get-authorization-token",
54-
"--domain", "wafflestudio", "--domain-owner", "405906814034",
55-
"--query", "authorizationToken", "--region", "ap-northeast-1", "--output", "text",
56-
).start().inputStream.bufferedReader().readText().trim()
57-
url = uri("https://wafflestudio-405906814034.d.codeartifact.ap-northeast-1.amazonaws.com/maven/spring-waffle/")
50+
url = uri("https://maven.pkg.github.com/wafflestudio/spring-waffle")
5851
credentials {
59-
username = "aws"
60-
password = authToken
52+
username = "wafflestudio"
53+
password = findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") ?: ""
6154
}
6255
}
6356
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ rootProject.name = "waffle-spring"
33
include(
44
"spring-boot-starter-waffle",
55
"spring-boot-starter-waffle-secret-manager",
6+
"spring-boot-starter-waffle-oci-vault",
67
"truffle",
78
"truffle:truffle-core",
89
"truffle:truffle-logback",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# spring-boot-starter-waffle-oci-vault
2+
3+
Loads OCI Vault Secrets into Spring `Environment` at startup (as an `EnvironmentPostProcessor`).
4+
5+
## Properties
6+
7+
Required:
8+
- `oci.vault.secret-ids`: Comma-separated secret OCIDs (`ocid1.vaultsecret...`).
9+
- `oci.vault.region`: Region id (default: `ap-chuncheon-1`).
10+
11+
Auth:
12+
- `oci.auth.type`: `auto` (default), `instance_principal`, or `config`.
13+
- `config`: Uses OCI config file credentials.
14+
- `instance_principal`: Uses Instance Principal (Dynamic Group) credentials.
15+
- `auto`: Tries config file credentials first; if it fails, falls back to Instance Principal.
16+
17+
Config-file auth options:
18+
- `oci.config.path`: Path to OCI config file (default: `~/.oci/config`).
19+
- `oci.config.profile`: Profile name (default: `DEFAULT`).
20+
21+
## Secret Format
22+
23+
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.
24+
25+
## Notes
26+
27+
- OKE Workload Identity is an Enhanced cluster feature and not implemented by this starter at the moment.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
group = "com.wafflestudio.spring"
2+
3+
dependencies {
4+
implementation("org.springframework.boot:spring-boot")
5+
6+
implementation("com.oracle.oci.sdk:oci-java-sdk-secrets:3.80.1")
7+
implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1")
8+
9+
testImplementation(kotlin("test"))
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.springframework.boot.EnvironmentPostProcessor=com.wafflestudio.spring.ocivault.config.OciVaultEnvironmentPostProcessor

0 commit comments

Comments
 (0)