From 68846065fdc0c9819f10c8e81a8d502e10bf5b61 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 14:35:29 +0900 Subject: [PATCH 01/21] =?UTF-8?q?mongodb=20username,=20password=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/main/resources/application-common.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/main/resources/application-common.yml b/core/src/main/resources/application-common.yml index e36507a5..f0b779f9 100644 --- a/core/src/main/resources/application-common.yml +++ b/core/src/main/resources/application-common.yml @@ -3,6 +3,8 @@ spring: default: local mongodb: uri: + username: + password: data: redis: repositories: From c714cf575c2ca018666ffa8ffacb16d28d112e8f Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 16:58:47 +0900 Subject: [PATCH 02/21] =?UTF-8?q?aws->oci=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/_deploy-native.yml | 50 ++++----- .github/workflows/_deploy.yml | 49 ++++----- .github/workflows/ci.yml | 9 +- .github/workflows/deploy-api-dev-native.yml | 6 +- .github/workflows/deploy-api-dev.yml | 6 +- .github/workflows/deploy-api-prod-native.yml | 6 +- .github/workflows/deploy-api-prod.yml | 6 +- .github/workflows/deploy-batch-dev-native.yml | 6 +- .github/workflows/deploy-batch-dev.yml | 6 +- .../workflows/deploy-batch-prod-native.yml | 6 +- .github/workflows/deploy-batch-prod.yml | 6 +- .github/workflows/deploy-manual-native.yml | 11 +- .github/workflows/deploy-manual.yml | 10 +- api/Dockerfile | 2 +- api/Dockerfile-native | 2 +- batch/Dockerfile | 2 +- batch/Dockerfile-native | 2 +- build.gradle.kts | 58 +++------- core/build.gradle.kts | 9 +- .../src/main/kotlin/common/mail/MailClient.kt | 83 +++++++++----- .../kotlin/common/storage/StorageClient.kt | 102 +++++++++++------- .../kotlin/common/storage/StorageService.kt | 2 +- core/src/main/kotlin/config/OciConfig.kt | 25 +++++ .../src/main/resources/application-common.yml | 8 +- 24 files changed, 257 insertions(+), 215 deletions(-) create mode 100644 core/src/main/kotlin/config/OciConfig.kt diff --git a/.github/workflows/_deploy-native.yml b/.github/workflows/_deploy-native.yml index 6c1703cf..4098ee82 100644 --- a/.github/workflows/_deploy-native.yml +++ b/.github/workflows/_deploy-native.yml @@ -3,7 +3,7 @@ name: deploy-native-template on: workflow_call: inputs: - ecr_repository: + ocir_repository: required: true type: string dockerfile: @@ -11,9 +11,9 @@ on: type: string description: 'Dockerfile path (e.g., api/Dockerfile-native)' secrets: - AWS_ACCESS_KEY_ID: + OCI_CLI_KEY_CONTENT: required: true - AWS_SECRET_ACCESS_KEY: + OCI_AUTH_TOKEN: required: true jobs: @@ -24,43 +24,33 @@ jobs: env: IMAGE_TAG: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }} - ECR_REGISTRY: 405906814034.dkr.ecr.ap-northeast-2.amazonaws.com - ECR_REPOSITORY: ${{ inputs.ecr_repository }} + OCIR_REGISTRY: yny.ocir.io + OCIR_REPOSITORY: ${{ inputs.ocir_repository }} + OCI_CLI_USER: ocid1.user.oc1..aaaaaaaafispxlhint36zqi2zsobz2svs7egdadtzezmpax3hsumz5ogpnma + OCI_CLI_TENANCY: ocid1.tenancy.oc1..aaaaaaaajzflhdho73h4er3roso57ebq4jpwt7n5uxg7rrgdnzmjtyeaogxa + OCI_CLI_FINGERPRINT: "84:0b:80:25:cd:8e:77:b3:77:e5:f5:0f:59:9d:aa:26" + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_CLI_REGION: ap-chuncheon-1 steps: - name: Checkout uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + - name: Login to OCIR + uses: oracle-actions/login-ocir@v1.3.0 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 + auth_token: ${{ secrets.OCI_AUTH_TOKEN }} - - name: Login to ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Get and save Auth Token for CodeArtifact - id: get-save-codeartifact-auth-token - run: | - aws codeartifact get-authorization-token \ - --domain wafflestudio \ - --domain-owner 405906814034 \ - --query authorizationToken \ - --region ap-northeast-1 \ - --output text > .codeartifact_token - - - name: Docker build, tag, and push image to ECR + - name: Docker build, tag, and push image to OCIR id: build-push-image run: | + echo "${{ github.token }}" > .github_token docker build \ - --secret id=codeartifact_token,src=./.codeartifact_token \ + --secret id=github_token,src=./.github_token \ -f ${{ inputs.dockerfile }} \ - -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ + -t $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG \ . \ --platform linux/arm64 - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT - + rm -f .github_token + docker push $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG + echo "image=$OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index 976cb448..55ace68d 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -3,7 +3,7 @@ name: deploy-template on: workflow_call: inputs: - ecr_repository: + ocir_repository: required: true type: string dockerfile: @@ -11,9 +11,9 @@ on: type: string description: 'Dockerfile path (e.g., api/Dockerfile)' secrets: - AWS_ACCESS_KEY_ID: + OCI_CLI_KEY_CONTENT: required: true - AWS_SECRET_ACCESS_KEY: + OCI_AUTH_TOKEN: required: true jobs: @@ -24,42 +24,33 @@ jobs: env: IMAGE_TAG: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }} - ECR_REGISTRY: 405906814034.dkr.ecr.ap-northeast-2.amazonaws.com - ECR_REPOSITORY: ${{ inputs.ecr_repository }} + OCIR_REGISTRY: yny.ocir.io + OCIR_REPOSITORY: ${{ inputs.ocir_repository }} + OCI_CLI_USER: ocid1.user.oc1..aaaaaaaafispxlhint36zqi2zsobz2svs7egdadtzezmpax3hsumz5ogpnma + OCI_CLI_TENANCY: ocid1.tenancy.oc1..aaaaaaaajzflhdho73h4er3roso57ebq4jpwt7n5uxg7rrgdnzmjtyeaogxa + OCI_CLI_FINGERPRINT: "84:0b:80:25:cd:8e:77:b3:77:e5:f5:0f:59:9d:aa:26" + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_CLI_REGION: ap-chuncheon-1 steps: - name: Checkout uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 + - name: Login to OCIR + uses: oracle-actions/login-ocir@v1.3.0 with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 + auth_token: ${{ secrets.OCI_AUTH_TOKEN }} - - name: Login to ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Get and save Auth Token for CodeArtifact - id: get-save-codeartifact-auth-token - run: | - aws codeartifact get-authorization-token \ - --domain wafflestudio \ - --domain-owner 405906814034 \ - --query authorizationToken \ - --region ap-northeast-1 \ - --output text > .codeartifact_token - - - name: Docker build, tag, and push image to ECR + - name: Docker build, tag, and push image to OCIR id: build-push-image run: | + echo "${{ github.token }}" > .github_token docker build \ - --secret id=codeartifact_token,src=./.codeartifact_token \ + --secret id=github_token,src=./.github_token \ -f ${{ inputs.dockerfile }} \ - -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \ + -t $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG \ . \ --platform linux/arm64 - docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + rm -f .github_token + docker push $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG + echo "image=$OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3c2570a..2a1b721c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,13 +14,6 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ap-northeast-2 - - name: Set up JDK 25 uses: actions/setup-java@v4 with: @@ -28,5 +21,7 @@ jobs: distribution: 'temurin' - name: Run Tests + env: + GITHUB_TOKEN: ${{ github.token }} run: | ./gradlew clean test -x processAot -x processTestAot diff --git a/.github/workflows/deploy-api-dev-native.yml b/.github/workflows/deploy-api-dev-native.yml index 739e19e9..16621f05 100644 --- a/.github/workflows/deploy-api-dev-native.yml +++ b/.github/workflows/deploy-api-dev-native.yml @@ -8,8 +8,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy-native.yml with: - ecr_repository: snutt-dev/snutt-timetable + ocir_repository: snutt-dev/snutt-timetable dockerfile: api/Dockerfile-native secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-api-dev.yml b/.github/workflows/deploy-api-dev.yml index fd7362c5..8202edc1 100644 --- a/.github/workflows/deploy-api-dev.yml +++ b/.github/workflows/deploy-api-dev.yml @@ -8,8 +8,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy.yml with: - ecr_repository: snutt-dev/snutt-timetable + ocir_repository: snutt-dev/snutt-timetable dockerfile: api/Dockerfile secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-api-prod-native.yml b/.github/workflows/deploy-api-prod-native.yml index e9d322b9..5c351350 100644 --- a/.github/workflows/deploy-api-prod-native.yml +++ b/.github/workflows/deploy-api-prod-native.yml @@ -8,11 +8,11 @@ jobs: deploy: uses: ./.github/workflows/_deploy-native.yml with: - ecr_repository: snutt-prod/snutt-timetable + ocir_repository: snutt-prod/snutt-timetable dockerfile: api/Dockerfile-native secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} notify: needs: deploy diff --git a/.github/workflows/deploy-api-prod.yml b/.github/workflows/deploy-api-prod.yml index e84b126c..0ecb52d9 100644 --- a/.github/workflows/deploy-api-prod.yml +++ b/.github/workflows/deploy-api-prod.yml @@ -8,11 +8,11 @@ jobs: deploy: uses: ./.github/workflows/_deploy.yml with: - ecr_repository: snutt-prod/snutt-timetable + ocir_repository: snutt-prod/snutt-timetable dockerfile: api/Dockerfile secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} notify: needs: deploy diff --git a/.github/workflows/deploy-batch-dev-native.yml b/.github/workflows/deploy-batch-dev-native.yml index a52efb03..d244109a 100644 --- a/.github/workflows/deploy-batch-dev-native.yml +++ b/.github/workflows/deploy-batch-dev-native.yml @@ -8,8 +8,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy-native.yml with: - ecr_repository: snutt-dev/snutt-timetable-batch + ocir_repository: snutt-dev/snutt-timetable-batch dockerfile: batch/Dockerfile-native secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-batch-dev.yml b/.github/workflows/deploy-batch-dev.yml index df0841ad..b0cd03a9 100644 --- a/.github/workflows/deploy-batch-dev.yml +++ b/.github/workflows/deploy-batch-dev.yml @@ -8,8 +8,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy.yml with: - ecr_repository: snutt-dev/snutt-timetable-batch + ocir_repository: snutt-dev/snutt-timetable-batch dockerfile: batch/Dockerfile secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-batch-prod-native.yml b/.github/workflows/deploy-batch-prod-native.yml index 0413702c..17e64a0e 100644 --- a/.github/workflows/deploy-batch-prod-native.yml +++ b/.github/workflows/deploy-batch-prod-native.yml @@ -8,8 +8,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy-native.yml with: - ecr_repository: snutt-prod/snutt-timetable-batch + ocir_repository: snutt-prod/snutt-timetable-batch dockerfile: batch/Dockerfile-native secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-batch-prod.yml b/.github/workflows/deploy-batch-prod.yml index 62b0f65a..16d70271 100644 --- a/.github/workflows/deploy-batch-prod.yml +++ b/.github/workflows/deploy-batch-prod.yml @@ -8,8 +8,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy.yml with: - ecr_repository: snutt-prod/snutt-timetable-batch + ocir_repository: snutt-prod/snutt-timetable-batch dockerfile: batch/Dockerfile secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-manual-native.yml b/.github/workflows/deploy-manual-native.yml index f8881e92..ab7967e8 100644 --- a/.github/workflows/deploy-manual-native.yml +++ b/.github/workflows/deploy-manual-native.yml @@ -3,8 +3,8 @@ name: Deploy Manual Native on: workflow_dispatch: inputs: - ecr_repository: - description: 'ECR 리포지토리 (예: snutt-dev/snutt-timetable)' + ocir_repository: + description: 'OCIR 리포지토리 (예: snutt-dev/snutt-timetable)' required: true type: string dockerfile: @@ -16,9 +16,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy-native.yml with: - ecr_repository: ${{ inputs.ecr_repository }} + ocir_repository: ${{ inputs.ocir_repository }} dockerfile: ${{ inputs.dockerfile }} secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml index f1a7fa69..b8ade180 100644 --- a/.github/workflows/deploy-manual.yml +++ b/.github/workflows/deploy-manual.yml @@ -3,8 +3,8 @@ name: Deploy Manual on: workflow_dispatch: inputs: - ecr_repository: - description: 'ECR 리포지토리 (예: snutt-dev/snutt-timetable)' + ocir_repository: + description: 'OCIR 리포지토리 (예: snutt-dev/snutt-timetable)' required: true type: string dockerfile: @@ -16,8 +16,8 @@ jobs: deploy: uses: ./.github/workflows/_deploy.yml with: - ecr_repository: ${{ inputs.ecr_repository }} + ocir_repository: ${{ inputs.ocir_repository }} dockerfile: ${{ inputs.dockerfile }} secrets: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} + OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/api/Dockerfile b/api/Dockerfile index dffe9e2d..2095d4e8 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -2,6 +2,6 @@ FROM ghcr.io/graalvm/jdk-community:25 WORKDIR /app COPY . /app RUN microdnf install -y findutils --nodocs -RUN --mount=type=secret,id=codeartifact_token ./gradlew :api:bootJar -PcodeArtifactAuthToken=$(cat /run/secrets/codeartifact_token) +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :api:bootJar EXPOSE 8080 ENTRYPOINT java $JAVA_OPTS -jar api/build/libs/snutt-api.jar diff --git a/api/Dockerfile-native b/api/Dockerfile-native index 2efc93ef..ac75859a 100644 --- a/api/Dockerfile-native +++ b/api/Dockerfile-native @@ -2,7 +2,7 @@ FROM container-registry.oracle.com/graalvm/native-image:25 AS builder WORKDIR /app COPY . /app -RUN --mount=type=secret,id=codeartifact_token ./gradlew :api:nativeCompile -PcodeArtifactAuthToken=$(cat /run/secrets/codeartifact_token) +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :api:nativeCompile # Runtime stage FROM docker.io/debian:stable-slim diff --git a/batch/Dockerfile b/batch/Dockerfile index 073c517e..8f5bab92 100644 --- a/batch/Dockerfile +++ b/batch/Dockerfile @@ -2,5 +2,5 @@ FROM ghcr.io/graalvm/jdk-community:25 WORKDIR /app COPY . /app RUN microdnf install -y findutils --nodocs -RUN --mount=type=secret,id=codeartifact_token ./gradlew :batch:bootJar -PcodeArtifactAuthToken=$(cat /run/secrets/codeartifact_token) +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :batch:bootJar ENTRYPOINT java $JAVA_OPTS -jar batch/build/libs/snutt-batch.jar --job.name=${JOB_NAME} diff --git a/batch/Dockerfile-native b/batch/Dockerfile-native index beb4895b..6d00262f 100644 --- a/batch/Dockerfile-native +++ b/batch/Dockerfile-native @@ -2,7 +2,7 @@ FROM container-registry.oracle.com/graalvm/native-image:25 AS builder WORKDIR /app COPY . /app -RUN --mount=type=secret,id=codeartifact_token ./gradlew :batch:nativeCompile -PcodeArtifactAuthToken=$(cat /run/secrets/codeartifact_token) +RUN --mount=type=secret,id=github_token GITHUB_TOKEN=$(cat /run/secrets/github_token) ./gradlew :batch:nativeCompile # Runtime stage FROM docker.io/debian:stable-slim diff --git a/build.gradle.kts b/build.gradle.kts index afbb791b..25a0fc07 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,5 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import java.io.ByteArrayOutputStream plugins { id("org.graalvm.buildtools.native") version "0.11.3" apply false @@ -17,7 +16,22 @@ version = "0.0.1-SNAPSHOT" allprojects { repositories { mavenCentral() - mavenCodeArtifact() + maven { + url = uri("https://maven.pkg.github.com/wafflestudio/spring-waffle") + credentials { + username = "wafflestudio" + password = findProperty("gpr.key") as String? + ?: System.getenv("GITHUB_TOKEN") + ?: runCatching { + ProcessBuilder("gh", "auth", "token") + .start() + .inputStream + .bufferedReader() + .readText() + .trim() + }.getOrDefault("") + } + } mavenLocal() } } @@ -66,43 +80,3 @@ subprojects { useJUnitPlatform() } } - -interface InjectedExecOps { - @get:Inject val execOps: ExecOperations -} - -fun RepositoryHandler.mavenCodeArtifact() { - val injected = project.objects.newInstance() - maven { - val authToken = - properties["codeArtifactAuthToken"] as String? ?: ByteArrayOutputStream().use { - runCatching { - injected.execOps.exec { - commandLine = - listOf( - "aws", - "codeartifact", - "get-authorization-token", - "--domain", - "wafflestudio", - "--domain-owner", - "405906814034", - "--query", - "authorizationToken", - "--region", - "ap-northeast-1", - "--output", - "text", - ) - standardOutput = it - } - } - it.toString() - } - url = uri("https://wafflestudio-405906814034.d.codeartifact.ap-northeast-1.amazonaws.com/maven/spring-waffle/") - credentials { - username = "aws" - password = authToken - } - } -} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f15bcb69..8ab01135 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -16,9 +16,12 @@ dependencies { api("org.springframework.boot:spring-boot-starter-data-redis") implementation("org.springframework.security:spring-security-crypto") - implementation("software.amazon.awssdk:s3:2.32.14") - implementation("software.amazon.awssdk:ses:2.32.14") - implementation("com.wafflestudio.spring:spring-boot-starter-waffle:2.0.0") + implementation("com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.80.1") + implementation("com.oracle.oci.sdk:oci-java-sdk-emaildataplane: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") + implementation("com.wafflestudio.spring:spring-boot-starter-waffle-oci-vault:2.1.0-SNAPSHOT") + implementation("com.wafflestudio.spring.truffle:spring-boot-starter-truffle:2.1.0-SNAPSHOT") implementation("com.google.firebase:firebase-admin:9.7.0") implementation("io.jsonwebtoken:jjwt-api:0.13.0") implementation("io.jsonwebtoken:jjwt-impl:0.13.0") diff --git a/core/src/main/kotlin/common/mail/MailClient.kt b/core/src/main/kotlin/common/mail/MailClient.kt index 7081c5e6..6c95b3e7 100644 --- a/core/src/main/kotlin/common/mail/MailClient.kt +++ b/core/src/main/kotlin/common/mail/MailClient.kt @@ -1,18 +1,29 @@ package com.wafflestudio.snutt.common.mail -import kotlinx.coroutines.future.await +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider +import com.oracle.bmc.emaildataplane.EmailDPClient +import com.oracle.bmc.emaildataplane.model.EmailAddress +import com.oracle.bmc.emaildataplane.model.Recipients +import com.oracle.bmc.emaildataplane.model.Sender +import com.oracle.bmc.emaildataplane.model.SubmitEmailDetails +import com.oracle.bmc.emaildataplane.requests.SubmitEmailRequest +import com.wafflestudio.snutt.config.OciConfig +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component -import software.amazon.awssdk.regions.Region.AP_NORTHEAST_2 -import software.amazon.awssdk.services.ses.SesAsyncClient -import software.amazon.awssdk.services.ses.model.Body -import software.amazon.awssdk.services.ses.model.Content -import software.amazon.awssdk.services.ses.model.Destination -import software.amazon.awssdk.services.ses.model.Message -import software.amazon.awssdk.services.ses.model.SendEmailRequest @Component -class MailClient { - private val sesClient = SesAsyncClient.builder().region(AP_NORTHEAST_2).build() +class MailClient( + authProvider: BasicAuthenticationDetailsProvider, + @Value("\${oci.email.compartment-id}") private val compartmentId: String, +) { + private val emailClient = + EmailDPClient + .builder() + .region(OciConfig.REGION) + .build(authProvider) + val sourceEmail = "snutt@wafflestudio.com" suspend fun sendMail( @@ -20,20 +31,42 @@ class MailClient { subject: String, body: String, ) { - val dest = Destination.builder().toAddresses(to).build() - val message = - Message - .builder() - .subject(Content.builder().data(subject).build()) - .body(Body.builder().html(Content.builder().data(body).build()).build()) - .build() - val request = - SendEmailRequest - .builder() - .destination(dest) - .message(message) - .source(sourceEmail) - .build() - sesClient.sendEmail(request).await() + withContext(Dispatchers.IO) { + val emailDetails = + SubmitEmailDetails + .builder() + .sender( + Sender + .builder() + .senderAddress( + EmailAddress + .builder() + .email(sourceEmail) + .name("SNUTT") + .build(), + ).compartmentId(compartmentId) + .build(), + ).recipients( + Recipients + .builder() + .to( + listOf( + EmailAddress + .builder() + .email(to) + .build(), + ), + ).build(), + ).subject(subject) + .bodyHtml(body) + .build() + + emailClient.submitEmail( + SubmitEmailRequest + .builder() + .submitEmailDetails(emailDetails) + .build(), + ) + } } } diff --git a/core/src/main/kotlin/common/storage/StorageClient.kt b/core/src/main/kotlin/common/storage/StorageClient.kt index b9367099..55a291a3 100644 --- a/core/src/main/kotlin/common/storage/StorageClient.kt +++ b/core/src/main/kotlin/common/storage/StorageClient.kt @@ -1,18 +1,32 @@ package com.wafflestudio.snutt.common.storage +import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider +import com.oracle.bmc.objectstorage.ObjectStorageClient +import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails +import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails.AccessType +import com.oracle.bmc.objectstorage.requests.CreatePreauthenticatedRequestRequest +import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest +import com.wafflestudio.snutt.config.OciConfig import jakarta.annotation.PostConstruct +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.springframework.stereotype.Component -import software.amazon.awssdk.regions.Region.AP_NORTHEAST_2 -import software.amazon.awssdk.services.s3.internal.signing.DefaultS3Presigner -import software.amazon.awssdk.services.s3.model.GetObjectRequest -import software.amazon.awssdk.services.s3.model.PutObjectRequest -import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest -import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest import java.time.Duration +import java.time.Instant +import java.util.Date +import java.util.UUID @Component -class StorageClient { - private val presigner = DefaultS3Presigner.builder().region(AP_NORTHEAST_2).build() +class StorageClient( + authProvider: BasicAuthenticationDetailsProvider, +) { + private val objectStorageClient = + ObjectStorageClient + .builder() + .region(OciConfig.REGION) + .build(authProvider) + + private lateinit var namespace: String companion object { lateinit var instance: StorageClient @@ -20,40 +34,49 @@ class StorageClient { private val uploadDuration = Duration.ofMinutes(10) private val getDuration = Duration.ofHours(1) + private const val ENDPOINT = "https://objectstorage.ap-chuncheon-1.oraclecloud.com" } @PostConstruct fun init() { instance = this + namespace = objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()).value } suspend fun generatePutSignedUris( storageType: StorageType, key: String, - ): String { - val request = - PutObjectRequest - .builder() - .bucket(storageType.bucketName) - .key(key) - .build() + ): String = + withContext(Dispatchers.IO) { + val parDetails = + CreatePreauthenticatedRequestDetails + .builder() + .name("upload-${UUID.randomUUID()}") + .objectName(key) + .accessType(AccessType.ObjectWrite) + .timeExpires(Date.from(Instant.now().plus(uploadDuration))) + .build() - val presignRequest = - PutObjectPresignRequest - .builder() - .signatureDuration(uploadDuration) - .putObjectRequest(request) - .build() + val response = + objectStorageClient.createPreauthenticatedRequest( + CreatePreauthenticatedRequestRequest + .builder() + .namespaceName(namespace) + .bucketName(storageType.bucketName) + .createPreauthenticatedRequestDetails(parDetails) + .build(), + ) - return presigner.presignPutObject(presignRequest).url().toString() - } + "$ENDPOINT${response.preauthenticatedRequest.accessUri}" + } fun generateGetUri(originUri: String): String { - val bucketName = originUri.substringAfter("s3://").substringBefore("/") - val key = originUri.substringAfter(bucketName).substringAfter("/") + val withoutScheme = originUri.substringAfter("oci://") + val bucketName = withoutScheme.substringBefore("/") + val key = withoutScheme.substringAfter("/") if (StorageType.from(bucketName).acl == Acl.PUBLIC_READ) { - return "https://$bucketName.s3.${AP_NORTHEAST_2.id()}.amazonaws.com/$key" + return "$ENDPOINT/n/$namespace/b/$bucketName/o/$key" } return generateGetSignedUri(bucketName, key) @@ -63,21 +86,26 @@ class StorageClient { bucketName: String, key: String, ): String { - val request = - GetObjectRequest + val parDetails = + CreatePreauthenticatedRequestDetails .builder() - .bucket(bucketName) - .key(key) + .name("download-${UUID.randomUUID()}") + .objectName(key) + .accessType(AccessType.ObjectRead) + .timeExpires(Date.from(Instant.now().plus(getDuration))) .build() - val presignRequest = - GetObjectPresignRequest - .builder() - .signatureDuration(getDuration) - .getObjectRequest(request) - .build() + val response = + objectStorageClient.createPreauthenticatedRequest( + CreatePreauthenticatedRequestRequest + .builder() + .namespaceName(namespace) + .bucketName(bucketName) + .createPreauthenticatedRequestDetails(parDetails) + .build(), + ) - return presigner.presignGetObject(presignRequest).url().toString() + return "$ENDPOINT${response.preauthenticatedRequest.accessUri}" } } diff --git a/core/src/main/kotlin/common/storage/StorageService.kt b/core/src/main/kotlin/common/storage/StorageService.kt index ed323646..56128d8a 100644 --- a/core/src/main/kotlin/common/storage/StorageService.kt +++ b/core/src/main/kotlin/common/storage/StorageService.kt @@ -27,7 +27,7 @@ class StorageService( val key = "$path${UUID.randomUUID()}.${JPG.value}" val uploadUri = storageClient.generatePutSignedUris(storageType, key) - val fileOriginUri = "s3://${storageType.bucketName}/$key" + val fileOriginUri = "oci://${storageType.bucketName}/$key" val fileUri = storageClient.generateGetUri(fileOriginUri) FileUploadUriDto( diff --git a/core/src/main/kotlin/config/OciConfig.kt b/core/src/main/kotlin/config/OciConfig.kt new file mode 100644 index 00000000..33279bc9 --- /dev/null +++ b/core/src/main/kotlin/config/OciConfig.kt @@ -0,0 +1,25 @@ +package com.wafflestudio.snutt.config + +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 org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile + +@Configuration +class OciConfig { + companion object { + val REGION: Region = Region.AP_CHUNCHEON_1 + } + + @Bean + @Profile("local", "test") + fun localAuthProvider(): BasicAuthenticationDetailsProvider = ConfigFileAuthenticationDetailsProvider("DEFAULT") + + @Bean + @Profile("dev", "prod") + fun workloadIdentityAuthProvider(): BasicAuthenticationDetailsProvider = + OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build() +} diff --git a/core/src/main/resources/application-common.yml b/core/src/main/resources/application-common.yml index f0b779f9..3e3da6f3 100644 --- a/core/src/main/resources/application-common.yml +++ b/core/src/main/resources/application-common.yml @@ -31,6 +31,10 @@ google: bundle-id: app-store-id: +oci: + email: + compartment-id: ocid1.compartment.oc1..aaaaaaaaxzo4fga6br76o3e34rshtsl6alzripmgdh7f4lg4u4tzezosypaq + oidc: facebook: app-id: @@ -52,7 +56,7 @@ api: snuttev: base-url: http://snutt-ev-api-dev.wafflestudio.com -secret-names: dev/snutt-timetable +oci-vault-secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqahycrebaagkzrbtah3mvjtsyrhfw6aoeaarw4iansbkra --- @@ -138,4 +142,4 @@ api: base-url: http://snutt-ev.snutt-prod.svc.cluster.local read-timeout: 5000 -secret-names: prod/snutt-timetable +oci-vault-secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqawhuzi5tw7l3zaqqmfqlorwfqi4kr3uhrlc6r2fio7a3a From d7d1e4163bfcdce04bd498599e5c86f4b28ace2c Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 17:12:23 +0900 Subject: [PATCH 03/21] fix actions --- .github/workflows/_deploy-native.yml | 11 +---------- .github/workflows/_deploy.yml | 11 +---------- .github/workflows/deploy-api-dev-native.yml | 1 - .github/workflows/deploy-api-dev.yml | 1 - .github/workflows/deploy-api-prod-native.yml | 1 - .github/workflows/deploy-api-prod.yml | 1 - .github/workflows/deploy-batch-dev-native.yml | 1 - .github/workflows/deploy-batch-dev.yml | 1 - .github/workflows/deploy-batch-prod-native.yml | 1 - .github/workflows/deploy-batch-prod.yml | 1 - .github/workflows/deploy-manual-native.yml | 1 - .github/workflows/deploy-manual.yml | 1 - 12 files changed, 2 insertions(+), 30 deletions(-) diff --git a/.github/workflows/_deploy-native.yml b/.github/workflows/_deploy-native.yml index 4098ee82..947ef030 100644 --- a/.github/workflows/_deploy-native.yml +++ b/.github/workflows/_deploy-native.yml @@ -11,8 +11,6 @@ on: type: string description: 'Dockerfile path (e.g., api/Dockerfile-native)' secrets: - OCI_CLI_KEY_CONTENT: - required: true OCI_AUTH_TOKEN: required: true @@ -26,20 +24,13 @@ jobs: BUILD_NUMBER: ${{ github.run_number }} OCIR_REGISTRY: yny.ocir.io OCIR_REPOSITORY: ${{ inputs.ocir_repository }} - OCI_CLI_USER: ocid1.user.oc1..aaaaaaaafispxlhint36zqi2zsobz2svs7egdadtzezmpax3hsumz5ogpnma - OCI_CLI_TENANCY: ocid1.tenancy.oc1..aaaaaaaajzflhdho73h4er3roso57ebq4jpwt7n5uxg7rrgdnzmjtyeaogxa - OCI_CLI_FINGERPRINT: "84:0b:80:25:cd:8e:77:b3:77:e5:f5:0f:59:9d:aa:26" - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} - OCI_CLI_REGION: ap-chuncheon-1 steps: - name: Checkout uses: actions/checkout@v4 - name: Login to OCIR - uses: oracle-actions/login-ocir@v1.3.0 - with: - auth_token: ${{ secrets.OCI_AUTH_TOKEN }} + run: echo "${{ secrets.OCI_AUTH_TOKEN }}" | docker login $OCIR_REGISTRY -u ax1dvc8vmenm/members/snutt-deployer --password-stdin - name: Docker build, tag, and push image to OCIR id: build-push-image diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index 55ace68d..ce97238a 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -11,8 +11,6 @@ on: type: string description: 'Dockerfile path (e.g., api/Dockerfile)' secrets: - OCI_CLI_KEY_CONTENT: - required: true OCI_AUTH_TOKEN: required: true @@ -26,20 +24,13 @@ jobs: BUILD_NUMBER: ${{ github.run_number }} OCIR_REGISTRY: yny.ocir.io OCIR_REPOSITORY: ${{ inputs.ocir_repository }} - OCI_CLI_USER: ocid1.user.oc1..aaaaaaaafispxlhint36zqi2zsobz2svs7egdadtzezmpax3hsumz5ogpnma - OCI_CLI_TENANCY: ocid1.tenancy.oc1..aaaaaaaajzflhdho73h4er3roso57ebq4jpwt7n5uxg7rrgdnzmjtyeaogxa - OCI_CLI_FINGERPRINT: "84:0b:80:25:cd:8e:77:b3:77:e5:f5:0f:59:9d:aa:26" - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} - OCI_CLI_REGION: ap-chuncheon-1 steps: - name: Checkout uses: actions/checkout@v4 - name: Login to OCIR - uses: oracle-actions/login-ocir@v1.3.0 - with: - auth_token: ${{ secrets.OCI_AUTH_TOKEN }} + run: echo "${{ secrets.OCI_AUTH_TOKEN }}" | docker login $OCIR_REGISTRY -u ax1dvc8vmenm/members/snutt-deployer --password-stdin - name: Docker build, tag, and push image to OCIR id: build-push-image diff --git a/.github/workflows/deploy-api-dev-native.yml b/.github/workflows/deploy-api-dev-native.yml index 16621f05..d5c367ac 100644 --- a/.github/workflows/deploy-api-dev-native.yml +++ b/.github/workflows/deploy-api-dev-native.yml @@ -11,5 +11,4 @@ jobs: ocir_repository: snutt-dev/snutt-timetable dockerfile: api/Dockerfile-native secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-api-dev.yml b/.github/workflows/deploy-api-dev.yml index 8202edc1..ce84533d 100644 --- a/.github/workflows/deploy-api-dev.yml +++ b/.github/workflows/deploy-api-dev.yml @@ -11,5 +11,4 @@ jobs: ocir_repository: snutt-dev/snutt-timetable dockerfile: api/Dockerfile secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-api-prod-native.yml b/.github/workflows/deploy-api-prod-native.yml index 5c351350..b02b06f5 100644 --- a/.github/workflows/deploy-api-prod-native.yml +++ b/.github/workflows/deploy-api-prod-native.yml @@ -11,7 +11,6 @@ jobs: ocir_repository: snutt-prod/snutt-timetable dockerfile: api/Dockerfile-native secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} notify: diff --git a/.github/workflows/deploy-api-prod.yml b/.github/workflows/deploy-api-prod.yml index 0ecb52d9..fd09b059 100644 --- a/.github/workflows/deploy-api-prod.yml +++ b/.github/workflows/deploy-api-prod.yml @@ -11,7 +11,6 @@ jobs: ocir_repository: snutt-prod/snutt-timetable dockerfile: api/Dockerfile secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} notify: diff --git a/.github/workflows/deploy-batch-dev-native.yml b/.github/workflows/deploy-batch-dev-native.yml index d244109a..90ee7484 100644 --- a/.github/workflows/deploy-batch-dev-native.yml +++ b/.github/workflows/deploy-batch-dev-native.yml @@ -11,5 +11,4 @@ jobs: ocir_repository: snutt-dev/snutt-timetable-batch dockerfile: batch/Dockerfile-native secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-batch-dev.yml b/.github/workflows/deploy-batch-dev.yml index b0cd03a9..36d3c598 100644 --- a/.github/workflows/deploy-batch-dev.yml +++ b/.github/workflows/deploy-batch-dev.yml @@ -11,5 +11,4 @@ jobs: ocir_repository: snutt-dev/snutt-timetable-batch dockerfile: batch/Dockerfile secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-batch-prod-native.yml b/.github/workflows/deploy-batch-prod-native.yml index 17e64a0e..06c0ad75 100644 --- a/.github/workflows/deploy-batch-prod-native.yml +++ b/.github/workflows/deploy-batch-prod-native.yml @@ -11,5 +11,4 @@ jobs: ocir_repository: snutt-prod/snutt-timetable-batch dockerfile: batch/Dockerfile-native secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-batch-prod.yml b/.github/workflows/deploy-batch-prod.yml index 16d70271..a720a072 100644 --- a/.github/workflows/deploy-batch-prod.yml +++ b/.github/workflows/deploy-batch-prod.yml @@ -11,5 +11,4 @@ jobs: ocir_repository: snutt-prod/snutt-timetable-batch dockerfile: batch/Dockerfile secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-manual-native.yml b/.github/workflows/deploy-manual-native.yml index ab7967e8..980312ce 100644 --- a/.github/workflows/deploy-manual-native.yml +++ b/.github/workflows/deploy-manual-native.yml @@ -19,5 +19,4 @@ jobs: ocir_repository: ${{ inputs.ocir_repository }} dockerfile: ${{ inputs.dockerfile }} secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml index b8ade180..4ba5edca 100644 --- a/.github/workflows/deploy-manual.yml +++ b/.github/workflows/deploy-manual.yml @@ -19,5 +19,4 @@ jobs: ocir_repository: ${{ inputs.ocir_repository }} dockerfile: ${{ inputs.dockerfile }} secrets: - OCI_CLI_KEY_CONTENT: ${{ secrets.OCI_CLI_KEY_CONTENT }} OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} From 74325ffa5dcf8ff92844a1526c7abcdf47918d7a Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 17:16:49 +0900 Subject: [PATCH 04/21] hardcode compartmentId --- core/src/main/kotlin/common/mail/MailClient.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/src/main/kotlin/common/mail/MailClient.kt b/core/src/main/kotlin/common/mail/MailClient.kt index 6c95b3e7..0e6f273c 100644 --- a/core/src/main/kotlin/common/mail/MailClient.kt +++ b/core/src/main/kotlin/common/mail/MailClient.kt @@ -10,14 +10,13 @@ import com.oracle.bmc.emaildataplane.requests.SubmitEmailRequest import com.wafflestudio.snutt.config.OciConfig import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component @Component class MailClient( authProvider: BasicAuthenticationDetailsProvider, - @Value("\${oci.email.compartment-id}") private val compartmentId: String, ) { + private val compartmentId = "ocid1.compartment.oc1..aaaaaaaaxzo4fga6br76o3e34rshtsl6alzripmgdh7f4lg4u4tzezosypaq" private val emailClient = EmailDPClient .builder() From a1e99ad748a7864df1fa0c4afe2e879a9147e3f6 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 18:08:40 +0900 Subject: [PATCH 05/21] add ocir namespace --- .github/workflows/_deploy-native.yml | 7 ++++--- .github/workflows/_deploy.yml | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/_deploy-native.yml b/.github/workflows/_deploy-native.yml index 947ef030..e5ad70ec 100644 --- a/.github/workflows/_deploy-native.yml +++ b/.github/workflows/_deploy-native.yml @@ -23,6 +23,7 @@ jobs: IMAGE_TAG: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }} OCIR_REGISTRY: yny.ocir.io + OCIR_NAMESPACE: ax1dvc8vmenm OCIR_REPOSITORY: ${{ inputs.ocir_repository }} steps: @@ -39,9 +40,9 @@ jobs: docker build \ --secret id=github_token,src=./.github_token \ -f ${{ inputs.dockerfile }} \ - -t $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG \ + -t $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG \ . \ --platform linux/arm64 rm -f .github_token - docker push $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG - echo "image=$OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + docker push $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG + echo "image=$OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index ce97238a..16a1413a 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -23,6 +23,7 @@ jobs: IMAGE_TAG: ${{ github.run_number }} BUILD_NUMBER: ${{ github.run_number }} OCIR_REGISTRY: yny.ocir.io + OCIR_NAMESPACE: ax1dvc8vmenm OCIR_REPOSITORY: ${{ inputs.ocir_repository }} steps: @@ -39,9 +40,9 @@ jobs: docker build \ --secret id=github_token,src=./.github_token \ -f ${{ inputs.dockerfile }} \ - -t $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG \ + -t $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG \ . \ --platform linux/arm64 rm -f .github_token - docker push $OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG - echo "image=$OCIR_REGISTRY/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + docker push $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG + echo "image=$OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT From 92697531d2dc13dea01c77e8df401903c82fbf9e Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 18:58:45 +0900 Subject: [PATCH 06/21] mock storageClient, mailClient --- api/src/test/kotlin/BaseIntegTest.kt | 4 ++++ core/src/main/kotlin/common/storage/StorageClient.kt | 9 ++++----- core/src/main/kotlin/config/OciConfig.kt | 3 ++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/api/src/test/kotlin/BaseIntegTest.kt b/api/src/test/kotlin/BaseIntegTest.kt index c6d4daaa..e7620881 100644 --- a/api/src/test/kotlin/BaseIntegTest.kt +++ b/api/src/test/kotlin/BaseIntegTest.kt @@ -1,3 +1,6 @@ +import com.ninjasquad.springmockk.MockkBean +import com.wafflestudio.snutt.common.mail.MailClient +import com.wafflestudio.snutt.common.storage.StorageClient import io.kotest.core.spec.style.WordSpec import mock.MockMongoDB import org.springframework.boot.test.context.SpringBootTest @@ -5,6 +8,7 @@ import org.springframework.context.annotation.Import @SpringBootTest @Import(MockMongoDB::class) +@MockkBean(types = [StorageClient::class, MailClient::class], relaxed = true) abstract class BaseIntegTest( body: BaseIntegTest.() -> Unit = {}, ) : WordSpec() { diff --git a/core/src/main/kotlin/common/storage/StorageClient.kt b/core/src/main/kotlin/common/storage/StorageClient.kt index 55a291a3..0839a19c 100644 --- a/core/src/main/kotlin/common/storage/StorageClient.kt +++ b/core/src/main/kotlin/common/storage/StorageClient.kt @@ -7,7 +7,6 @@ import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails.A import com.oracle.bmc.objectstorage.requests.CreatePreauthenticatedRequestRequest import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest import com.wafflestudio.snutt.config.OciConfig -import jakarta.annotation.PostConstruct import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.springframework.stereotype.Component @@ -26,7 +25,9 @@ class StorageClient( .region(OciConfig.REGION) .build(authProvider) - private lateinit var namespace: String + private val namespace: String by lazy { + objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()).value + } companion object { lateinit var instance: StorageClient @@ -37,10 +38,8 @@ class StorageClient( private const val ENDPOINT = "https://objectstorage.ap-chuncheon-1.oraclecloud.com" } - @PostConstruct - fun init() { + init { instance = this - namespace = objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()).value } suspend fun generatePutSignedUris( diff --git a/core/src/main/kotlin/config/OciConfig.kt b/core/src/main/kotlin/config/OciConfig.kt index 33279bc9..55b6bc74 100644 --- a/core/src/main/kotlin/config/OciConfig.kt +++ b/core/src/main/kotlin/config/OciConfig.kt @@ -9,13 +9,14 @@ import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @Configuration +@Profile("!test") class OciConfig { companion object { val REGION: Region = Region.AP_CHUNCHEON_1 } @Bean - @Profile("local", "test") + @Profile("local") fun localAuthProvider(): BasicAuthenticationDetailsProvider = ConfigFileAuthenticationDetailsProvider("DEFAULT") @Bean From fed468c86da7ccc4339d17c616c1416a6ce19b38 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 20:25:34 +0900 Subject: [PATCH 07/21] add jersey hk2 reflect-config --- .../jersey-hk2/reflect-config.json | 68 +++++++++++++++++++ .../META-INF/native-image/reflect-config.json | 12 ++++ 2 files changed, 80 insertions(+) create mode 100644 META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json diff --git a/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json b/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json new file mode 100644 index 00000000..e1234392 --- /dev/null +++ b/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json @@ -0,0 +1,68 @@ +[ + { + "name": "org.glassfish.hk2.internal.PerThreadContext", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.hk2.osgiresourcelocator.ServiceLoader", + "methods": [{ "name":"lookupProviderClasses", "parameterTypes":["java.lang.Class"] }] + }, + { + "name":"org.glassfish.jersey.inject.hk2.ContextInjectionResolverImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.glassfish.jersey.inject.hk2.Hk2InjectionManagerFactory", + "methods":[{"name":"","parameterTypes":[] }] + }, + { + "name":"org.glassfish.jersey.inject.hk2.Hk2RequestScope", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.glassfish.jersey.inject.hk2.InstanceSupplierFactoryBridge", + "methods":[{"name":"provide","parameterTypes":[] }] + }, + { + "name":"org.glassfish.jersey.inject.hk2.JerseyErrorService", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.glassfish.jersey.inject.hk2.RequestContext", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.ProxyUtilities$4", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.ProxyUtilities", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.DynamicConfigurationServiceImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + }, + { + "name":"org.jvnet.hk2.internal.ServiceLocatorRuntimeImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true + } +] diff --git a/core/src/main/resources/META-INF/native-image/reflect-config.json b/core/src/main/resources/META-INF/native-image/reflect-config.json index de72d579..1829ae8b 100644 --- a/core/src/main/resources/META-INF/native-image/reflect-config.json +++ b/core/src/main/resources/META-INF/native-image/reflect-config.json @@ -16,5 +16,17 @@ "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer$Binder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true } ] \ No newline at end of file From fde61706a725e9793dc82abeac606cae75a164b9 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 21:33:17 +0900 Subject: [PATCH 08/21] update OciConfig.kt --- core/src/main/kotlin/config/OciConfig.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/config/OciConfig.kt b/core/src/main/kotlin/config/OciConfig.kt index 55b6bc74..2d4461c9 100644 --- a/core/src/main/kotlin/config/OciConfig.kt +++ b/core/src/main/kotlin/config/OciConfig.kt @@ -3,7 +3,7 @@ package com.wafflestudio.snutt.config 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.auth.InstancePrincipalsAuthenticationDetailsProvider import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @@ -21,6 +21,6 @@ class OciConfig { @Bean @Profile("dev", "prod") - fun workloadIdentityAuthProvider(): BasicAuthenticationDetailsProvider = - OkeWorkloadIdentityAuthenticationDetailsProvider.builder().build() + fun instancePrincipalAuthProvider(): BasicAuthenticationDetailsProvider = + InstancePrincipalsAuthenticationDetailsProvider.builder().build() } From 4c620e4437a3dee8d2de9ef6e12862a5e3a7e7fa Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 23:36:22 +0900 Subject: [PATCH 09/21] use apikey instead --- core/src/main/kotlin/config/OciConfig.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/core/src/main/kotlin/config/OciConfig.kt b/core/src/main/kotlin/config/OciConfig.kt index 2d4461c9..df9f8818 100644 --- a/core/src/main/kotlin/config/OciConfig.kt +++ b/core/src/main/kotlin/config/OciConfig.kt @@ -3,7 +3,6 @@ package com.wafflestudio.snutt.config import com.oracle.bmc.Region import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider -import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @@ -16,11 +15,5 @@ class OciConfig { } @Bean - @Profile("local") - fun localAuthProvider(): BasicAuthenticationDetailsProvider = ConfigFileAuthenticationDetailsProvider("DEFAULT") - - @Bean - @Profile("dev", "prod") - fun instancePrincipalAuthProvider(): BasicAuthenticationDetailsProvider = - InstancePrincipalsAuthenticationDetailsProvider.builder().build() + fun ociAuthProvider(): BasicAuthenticationDetailsProvider = ConfigFileAuthenticationDetailsProvider("DEFAULT") } From 9d2304f72fb5199754c99498bb1a56f4b1c3a8e5 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 11 Feb 2026 23:49:10 +0900 Subject: [PATCH 10/21] add image tag updater --- .github/actions/update-image-tag/action.yml | 54 +++++++++++++++++++ .github/workflows/_deploy-native.yml | 14 +++++ .github/workflows/_deploy.yml | 14 +++++ .github/workflows/deploy-api-dev-native.yml | 2 + .github/workflows/deploy-api-dev.yml | 2 + .github/workflows/deploy-api-prod-native.yml | 2 + .github/workflows/deploy-api-prod.yml | 2 + .github/workflows/deploy-batch-dev-native.yml | 2 + .github/workflows/deploy-batch-dev.yml | 2 + .../workflows/deploy-batch-prod-native.yml | 2 + .github/workflows/deploy-batch-prod.yml | 2 + .github/workflows/deploy-manual-native.yml | 2 + .github/workflows/deploy-manual.yml | 2 + 13 files changed, 102 insertions(+) create mode 100644 .github/actions/update-image-tag/action.yml diff --git a/.github/actions/update-image-tag/action.yml b/.github/actions/update-image-tag/action.yml new file mode 100644 index 00000000..21a11124 --- /dev/null +++ b/.github/actions/update-image-tag/action.yml @@ -0,0 +1,54 @@ +name: Update image tag in waffle-world-oci +description: Updates the image tag in waffle-world-oci ArgoCD manifests + +inputs: + app-id: + required: true + private-key: + required: true + ocir-registry: + required: true + ocir-namespace: + required: true + ocir-repository: + required: true + image-tag: + required: true + +runs: + using: composite + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ inputs.app-id }} + private-key: ${{ inputs.private-key }} + owner: wafflestudio + repositories: waffle-world-oci + + - name: Update image tag + shell: bash + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + OCIR_IMAGE: ${{ inputs.ocir-registry }}/${{ inputs.ocir-namespace }}/${{ inputs.ocir-repository }} + OCIR_REPOSITORY: ${{ inputs.ocir-repository }} + IMAGE_TAG: ${{ inputs.image-tag }} + run: | + REPO="wafflestudio/waffle-world-oci" + DIR="argocd/$OCIR_REPOSITORY" + + for FILE_PATH in $(gh api "repos/$REPO/contents/$DIR" --jq '.[] | select(.name | endswith(".yaml") or endswith(".yml")) | .path'); do + CONTENT=$(gh api "repos/$REPO/contents/$FILE_PATH" --jq '.content' | base64 -d) + if echo "$CONTENT" | grep -q "image: $OCIR_IMAGE:"; then + UPDATED=$(echo "$CONTENT" | sed "s|image: $OCIR_IMAGE:[^ ]*|image: $OCIR_IMAGE:$IMAGE_TAG|g") + if [ "$CONTENT" != "$UPDATED" ]; then + SHA=$(gh api "repos/$REPO/contents/$FILE_PATH" --jq '.sha') + gh api --method PUT "repos/$REPO/contents/$FILE_PATH" \ + -f message="build: update $OCIR_REPOSITORY to $IMAGE_TAG" \ + -f content="$(echo "$UPDATED" | base64 -w 0)" \ + -f sha="$SHA" + echo "Updated $FILE_PATH" + fi + fi + done diff --git a/.github/workflows/_deploy-native.yml b/.github/workflows/_deploy-native.yml index e5ad70ec..2387c958 100644 --- a/.github/workflows/_deploy-native.yml +++ b/.github/workflows/_deploy-native.yml @@ -13,6 +13,10 @@ on: secrets: OCI_AUTH_TOKEN: required: true + DEPLOYER_APP_ID: + required: true + DEPLOYER_APP_PRIVATE_KEY: + required: true jobs: deploy: @@ -46,3 +50,13 @@ jobs: rm -f .github_token docker push $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG echo "image=$OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Update image tag in waffle-world-oci + uses: ./.github/actions/update-image-tag + with: + app-id: ${{ secrets.DEPLOYER_APP_ID }} + private-key: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + ocir-registry: ${{ env.OCIR_REGISTRY }} + ocir-namespace: ${{ env.OCIR_NAMESPACE }} + ocir-repository: ${{ env.OCIR_REPOSITORY }} + image-tag: ${{ env.IMAGE_TAG }} diff --git a/.github/workflows/_deploy.yml b/.github/workflows/_deploy.yml index 16a1413a..6889aff4 100644 --- a/.github/workflows/_deploy.yml +++ b/.github/workflows/_deploy.yml @@ -13,6 +13,10 @@ on: secrets: OCI_AUTH_TOKEN: required: true + DEPLOYER_APP_ID: + required: true + DEPLOYER_APP_PRIVATE_KEY: + required: true jobs: deploy: @@ -46,3 +50,13 @@ jobs: rm -f .github_token docker push $OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG echo "image=$OCIR_REGISTRY/$OCIR_NAMESPACE/$OCIR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT + + - name: Update image tag in waffle-world-oci + uses: ./.github/actions/update-image-tag + with: + app-id: ${{ secrets.DEPLOYER_APP_ID }} + private-key: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} + ocir-registry: ${{ env.OCIR_REGISTRY }} + ocir-namespace: ${{ env.OCIR_NAMESPACE }} + ocir-repository: ${{ env.OCIR_REPOSITORY }} + image-tag: ${{ env.IMAGE_TAG }} diff --git a/.github/workflows/deploy-api-dev-native.yml b/.github/workflows/deploy-api-dev-native.yml index d5c367ac..625f538f 100644 --- a/.github/workflows/deploy-api-dev-native.yml +++ b/.github/workflows/deploy-api-dev-native.yml @@ -12,3 +12,5 @@ jobs: dockerfile: api/Dockerfile-native secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-api-dev.yml b/.github/workflows/deploy-api-dev.yml index ce84533d..93155625 100644 --- a/.github/workflows/deploy-api-dev.yml +++ b/.github/workflows/deploy-api-dev.yml @@ -12,3 +12,5 @@ jobs: dockerfile: api/Dockerfile secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-api-prod-native.yml b/.github/workflows/deploy-api-prod-native.yml index b02b06f5..28f96001 100644 --- a/.github/workflows/deploy-api-prod-native.yml +++ b/.github/workflows/deploy-api-prod-native.yml @@ -12,6 +12,8 @@ jobs: dockerfile: api/Dockerfile-native secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} notify: needs: deploy diff --git a/.github/workflows/deploy-api-prod.yml b/.github/workflows/deploy-api-prod.yml index fd09b059..49ea59e0 100644 --- a/.github/workflows/deploy-api-prod.yml +++ b/.github/workflows/deploy-api-prod.yml @@ -12,6 +12,8 @@ jobs: dockerfile: api/Dockerfile secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} notify: needs: deploy diff --git a/.github/workflows/deploy-batch-dev-native.yml b/.github/workflows/deploy-batch-dev-native.yml index 90ee7484..d41fc66f 100644 --- a/.github/workflows/deploy-batch-dev-native.yml +++ b/.github/workflows/deploy-batch-dev-native.yml @@ -12,3 +12,5 @@ jobs: dockerfile: batch/Dockerfile-native secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-batch-dev.yml b/.github/workflows/deploy-batch-dev.yml index 36d3c598..1abdbd9a 100644 --- a/.github/workflows/deploy-batch-dev.yml +++ b/.github/workflows/deploy-batch-dev.yml @@ -12,3 +12,5 @@ jobs: dockerfile: batch/Dockerfile secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-batch-prod-native.yml b/.github/workflows/deploy-batch-prod-native.yml index 06c0ad75..75587949 100644 --- a/.github/workflows/deploy-batch-prod-native.yml +++ b/.github/workflows/deploy-batch-prod-native.yml @@ -12,3 +12,5 @@ jobs: dockerfile: batch/Dockerfile-native secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-batch-prod.yml b/.github/workflows/deploy-batch-prod.yml index a720a072..2d1162c5 100644 --- a/.github/workflows/deploy-batch-prod.yml +++ b/.github/workflows/deploy-batch-prod.yml @@ -12,3 +12,5 @@ jobs: dockerfile: batch/Dockerfile secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-manual-native.yml b/.github/workflows/deploy-manual-native.yml index 980312ce..ef80cf37 100644 --- a/.github/workflows/deploy-manual-native.yml +++ b/.github/workflows/deploy-manual-native.yml @@ -20,3 +20,5 @@ jobs: dockerfile: ${{ inputs.dockerfile }} secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} diff --git a/.github/workflows/deploy-manual.yml b/.github/workflows/deploy-manual.yml index 4ba5edca..07f7bee2 100644 --- a/.github/workflows/deploy-manual.yml +++ b/.github/workflows/deploy-manual.yml @@ -20,3 +20,5 @@ jobs: dockerfile: ${{ inputs.dockerfile }} secrets: OCI_AUTH_TOKEN: ${{ secrets.OCI_AUTH_TOKEN }} + DEPLOYER_APP_ID: ${{ secrets.DEPLOYER_APP_ID }} + DEPLOYER_APP_PRIVATE_KEY: ${{ secrets.DEPLOYER_APP_PRIVATE_KEY }} From ab7f921865251748b83771a92dfa0ec03e5e63e5 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Thu, 12 Feb 2026 13:37:37 +0900 Subject: [PATCH 11/21] fix image updater --- .github/actions/update-image-tag/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/update-image-tag/action.yml b/.github/actions/update-image-tag/action.yml index 21a11124..221e2792 100644 --- a/.github/actions/update-image-tag/action.yml +++ b/.github/actions/update-image-tag/action.yml @@ -36,7 +36,8 @@ runs: IMAGE_TAG: ${{ inputs.image-tag }} run: | REPO="wafflestudio/waffle-world-oci" - DIR="argocd/$OCIR_REPOSITORY" + ENV_DIR=$(dirname "$OCIR_REPOSITORY") + DIR="argocd/$ENV_DIR" for FILE_PATH in $(gh api "repos/$REPO/contents/$DIR" --jq '.[] | select(.name | endswith(".yaml") or endswith(".yml")) | .path'); do CONTENT=$(gh api "repos/$REPO/contents/$FILE_PATH" --jq '.content' | base64 -d) From 18a95d598b3a8ae56268219d3704fae2d36f1086 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Fri, 13 Feb 2026 10:15:20 +0900 Subject: [PATCH 12/21] remove workload identity deps: --- core/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8ab01135..0e0a6347 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,7 +19,6 @@ dependencies { implementation("com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.80.1") implementation("com.oracle.oci.sdk:oci-java-sdk-emaildataplane: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") implementation("com.wafflestudio.spring:spring-boot-starter-waffle-oci-vault:2.1.0-SNAPSHOT") implementation("com.wafflestudio.spring.truffle:spring-boot-starter-truffle:2.1.0-SNAPSHOT") implementation("com.google.firebase:firebase-admin:9.7.0") From 44e4606cda8d8b84158f446ea8a842b66c09f430 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 10:29:51 +0900 Subject: [PATCH 13/21] change vault secret property name --- core/src/main/resources/application-common.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/src/main/resources/application-common.yml b/core/src/main/resources/application-common.yml index 3e3da6f3..7a326c7b 100644 --- a/core/src/main/resources/application-common.yml +++ b/core/src/main/resources/application-common.yml @@ -56,10 +56,11 @@ api: snuttev: base-url: http://snutt-ev-api-dev.wafflestudio.com -oci-vault-secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqahycrebaagkzrbtah3mvjtsyrhfw6aoeaarw4iansbkra +oci: + vault: + secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqahycrebaagkzrbtah3mvjtsyrhfw6aoeaarw4iansbkra --- - spring: config: activate: @@ -68,7 +69,6 @@ logging: config: classpath:logback/local.xml --- - spring: config: activate: @@ -108,7 +108,6 @@ logging: org.springframework.data.mongodb.core.ReactiveMongoTemplate: DEBUG --- - spring: config: activate: @@ -142,4 +141,6 @@ api: base-url: http://snutt-ev.snutt-prod.svc.cluster.local read-timeout: 5000 -oci-vault-secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqawhuzi5tw7l3zaqqmfqlorwfqi4kr3uhrlc6r2fio7a3a +oci: + vault: + secret-ids: ocid1.vaultsecret.oc1.ap-chuncheon-1.amaaaaaat2m5lbqawhuzi5tw7l3zaqqmfqlorwfqi4kr3uhrlc6r2fio7a3a From e6cdcf834b1a36c5d35a66b0103869a014ee34f2 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 11:24:06 +0900 Subject: [PATCH 14/21] fix OciConfig.kt --- core/src/main/kotlin/config/OciConfig.kt | 55 +++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/config/OciConfig.kt b/core/src/main/kotlin/config/OciConfig.kt index df9f8818..b0a32492 100644 --- a/core/src/main/kotlin/config/OciConfig.kt +++ b/core/src/main/kotlin/config/OciConfig.kt @@ -1,19 +1,70 @@ package com.wafflestudio.snutt.config +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 org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Profile @Configuration @Profile("!test") -class OciConfig { +class OciConfig( + @Value("\${oci.auth.type:auto}") + private val authType: String, + @Value("\${oci.config.profile:DEFAULT}") + private val configProfile: String, + @Value("\${oci.config.path:}") + private val configPath: String, +) { companion object { val REGION: Region = Region.AP_CHUNCHEON_1 } @Bean - fun ociAuthProvider(): BasicAuthenticationDetailsProvider = ConfigFileAuthenticationDetailsProvider("DEFAULT") + fun ociAuthProvider(): BasicAuthenticationDetailsProvider = + when (authType.trim().lowercase()) { + "auto" -> + try { + instancePrincipalAuth() + } catch (e: Exception) { + log.info( + "OCI instance principal auth failed; falling back to config file auth (oci.auth.type=auto): {}", + e.message, + ) + configFileAuth() + } + + "config" -> configFileAuth() + "instance_principal" -> instancePrincipalAuth() + else -> + throw IllegalArgumentException( + "Unsupported oci.auth.type='${authType.trim()}'. Supported: auto, config, instance_principal", + ) + } + + private val log = LoggerFactory.getLogger(javaClass) + + private fun instancePrincipalAuth(): BasicAuthenticationDetailsProvider = + InstancePrincipalsAuthenticationDetailsProvider.builder().build() + + private fun configFileAuth(): BasicAuthenticationDetailsProvider { + val profile = configProfile.trim().ifEmpty { "DEFAULT" } + val path = configPath.trim().ifEmpty { "" } + if (path.isEmpty()) { + return ConfigFileAuthenticationDetailsProvider(profile) + } + + val configFile = ConfigFileReader.parse(expandHome(path), profile) + return ConfigFileAuthenticationDetailsProvider(configFile) + } + + private fun expandHome(path: String): String { + val home = System.getProperty("user.home") + return if (path == "~") home else path.replaceFirst(Regex("^~(?=/|$)"), home) + } } From 9ec60b07fb4b1365309d484387d57ecf3203a49d Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 11:33:32 +0900 Subject: [PATCH 15/21] fix OciConfig.kt --- core/src/main/kotlin/config/OciConfig.kt | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/core/src/main/kotlin/config/OciConfig.kt b/core/src/main/kotlin/config/OciConfig.kt index b0a32492..a141065e 100644 --- a/core/src/main/kotlin/config/OciConfig.kt +++ b/core/src/main/kotlin/config/OciConfig.kt @@ -1,6 +1,5 @@ package com.wafflestudio.snutt.config -import com.oracle.bmc.ConfigFileReader import com.oracle.bmc.Region import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider @@ -14,12 +13,10 @@ import org.springframework.context.annotation.Profile @Configuration @Profile("!test") class OciConfig( - @Value("\${oci.auth.type:auto}") + @param:Value("\${oci.auth.type:auto}") private val authType: String, - @Value("\${oci.config.profile:DEFAULT}") + @param:Value("\${oci.config.profile:DEFAULT}") private val configProfile: String, - @Value("\${oci.config.path:}") - private val configPath: String, ) { companion object { val REGION: Region = Region.AP_CHUNCHEON_1 @@ -54,17 +51,6 @@ class OciConfig( private fun configFileAuth(): BasicAuthenticationDetailsProvider { val profile = configProfile.trim().ifEmpty { "DEFAULT" } - val path = configPath.trim().ifEmpty { "" } - if (path.isEmpty()) { - return ConfigFileAuthenticationDetailsProvider(profile) - } - - val configFile = ConfigFileReader.parse(expandHome(path), profile) - return ConfigFileAuthenticationDetailsProvider(configFile) - } - - private fun expandHome(path: String): String { - val home = System.getProperty("user.home") - return if (path == "~") home else path.replaceFirst(Regex("^~(?=/|$)"), home) + return ConfigFileAuthenticationDetailsProvider(profile) } } From 04bac6a5107f337ac39dea68fd9e84a0744945e3 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 11:57:09 +0900 Subject: [PATCH 16/21] use oci config auth in local profile --- core/src/main/resources/application-common.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/resources/application-common.yml b/core/src/main/resources/application-common.yml index 7a326c7b..d42bb1ce 100644 --- a/core/src/main/resources/application-common.yml +++ b/core/src/main/resources/application-common.yml @@ -67,6 +67,9 @@ spring: on-profile: local logging: config: classpath:logback/local.xml +oci: + auth: + type: config --- spring: From 648d9cf2a85cb27e2e32ce99d2d43e8f1fa9ccf9 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 14 Feb 2026 17:46:22 +0900 Subject: [PATCH 17/21] move jersey-hk2 hints --- .../{ => kotlin-compat}/reflect-config.json | 14 +------------- .../jersey-hk2/reflect-config.json | 12 ++++++++++++ 2 files changed, 13 insertions(+), 13 deletions(-) rename core/src/main/resources/META-INF/native-image/{ => kotlin-compat}/reflect-config.json (55%) rename {META-INF => core/src/main/resources/META-INF}/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json (84%) diff --git a/core/src/main/resources/META-INF/native-image/reflect-config.json b/core/src/main/resources/META-INF/native-image/kotlin-compat/reflect-config.json similarity index 55% rename from core/src/main/resources/META-INF/native-image/reflect-config.json rename to core/src/main/resources/META-INF/native-image/kotlin-compat/reflect-config.json index 1829ae8b..7ea3d649 100644 --- a/core/src/main/resources/META-INF/native-image/reflect-config.json +++ b/core/src/main/resources/META-INF/native-image/kotlin-compat/reflect-config.json @@ -16,17 +16,5 @@ "allDeclaredFields": true, "allDeclaredMethods": true, "allDeclaredConstructors": true - }, - { - "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true - }, - { - "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer$Binder", - "allDeclaredFields": true, - "allDeclaredMethods": true, - "allDeclaredConstructors": true } -] \ No newline at end of file +] diff --git a/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json b/core/src/main/resources/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json similarity index 84% rename from META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json rename to core/src/main/resources/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json index e1234392..0f7742dd 100644 --- a/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json +++ b/core/src/main/resources/META-INF/native-image/org.glassfish.jersey.inject/jersey-hk2/reflect-config.json @@ -1,4 +1,16 @@ [ + { + "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, + { + "name": "org.glassfish.jersey.inject.hk2.JerseyClassAnalyzer$Binder", + "allDeclaredFields": true, + "allDeclaredMethods": true, + "allDeclaredConstructors": true + }, { "name": "org.glassfish.hk2.internal.PerThreadContext", "allDeclaredFields": true, From 53fb4a3a9e9ce9d8a4a292b1f16768c89dc199ac Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Wed, 18 Feb 2026 18:08:03 +0900 Subject: [PATCH 18/21] keep s3:// scheme for compatibility --- core/src/main/kotlin/common/storage/StorageClient.kt | 2 +- core/src/main/kotlin/common/storage/StorageService.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/kotlin/common/storage/StorageClient.kt b/core/src/main/kotlin/common/storage/StorageClient.kt index 0839a19c..d088d922 100644 --- a/core/src/main/kotlin/common/storage/StorageClient.kt +++ b/core/src/main/kotlin/common/storage/StorageClient.kt @@ -70,7 +70,7 @@ class StorageClient( } fun generateGetUri(originUri: String): String { - val withoutScheme = originUri.substringAfter("oci://") + val withoutScheme = originUri.substringAfter("s3://") val bucketName = withoutScheme.substringBefore("/") val key = withoutScheme.substringAfter("/") diff --git a/core/src/main/kotlin/common/storage/StorageService.kt b/core/src/main/kotlin/common/storage/StorageService.kt index 56128d8a..ed323646 100644 --- a/core/src/main/kotlin/common/storage/StorageService.kt +++ b/core/src/main/kotlin/common/storage/StorageService.kt @@ -27,7 +27,7 @@ class StorageService( val key = "$path${UUID.randomUUID()}.${JPG.value}" val uploadUri = storageClient.generatePutSignedUris(storageType, key) - val fileOriginUri = "oci://${storageType.bucketName}/$key" + val fileOriginUri = "s3://${storageType.bucketName}/$key" val fileUri = storageClient.generateGetUri(fileOriginUri) FileUploadUriDto( From d4a748afa8d80f02c653d80321ccc5e2463814c0 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Fri, 20 Feb 2026 19:16:02 +0900 Subject: [PATCH 19/21] use spring-waffle 2.1.0 --- core/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 0e0a6347..9dd47741 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,8 +19,8 @@ dependencies { implementation("com.oracle.oci.sdk:oci-java-sdk-objectstorage:3.80.1") implementation("com.oracle.oci.sdk:oci-java-sdk-emaildataplane:3.80.1") implementation("com.oracle.oci.sdk:oci-java-sdk-common-httpclient-jersey3:3.80.1") - implementation("com.wafflestudio.spring:spring-boot-starter-waffle-oci-vault:2.1.0-SNAPSHOT") - implementation("com.wafflestudio.spring.truffle:spring-boot-starter-truffle:2.1.0-SNAPSHOT") + implementation("com.wafflestudio.spring:spring-boot-starter-waffle-oci-vault:2.1.0") + implementation("com.wafflestudio.spring.truffle:spring-boot-starter-truffle:2.1.0") implementation("com.google.firebase:firebase-admin:9.7.0") implementation("io.jsonwebtoken:jjwt-api:0.13.0") implementation("io.jsonwebtoken:jjwt-impl:0.13.0") From e1d295e55464ec5c6a66be0039dc3be461f1a7d4 Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sun, 22 Feb 2026 02:59:27 +0900 Subject: [PATCH 20/21] use async oci sdk for StorageClient and MailClient --- .../main/kotlin/controller/AdminController.kt | 6 +- .../main/kotlin/controller/PopupController.kt | 7 +- .../common/extension/OciAsyncExtensions.kt | 34 +++++ .../src/main/kotlin/common/mail/MailClient.kt | 72 ++++++----- .../kotlin/common/storage/StorageClient.kt | 119 +++++++++--------- .../kotlin/common/storage/StorageService.kt | 35 ++++-- .../main/kotlin/popup/dto/PopupResponse.kt | 10 +- 7 files changed, 169 insertions(+), 114 deletions(-) create mode 100644 core/src/main/kotlin/common/extension/OciAsyncExtensions.kt diff --git a/api/src/main/kotlin/controller/AdminController.kt b/api/src/main/kotlin/controller/AdminController.kt index 03e76887..07aee5cd 100644 --- a/api/src/main/kotlin/controller/AdminController.kt +++ b/api/src/main/kotlin/controller/AdminController.kt @@ -101,7 +101,11 @@ class AdminController( @PostMapping("/popups") suspend fun postPopup( @RequestBody body: PostPopupRequest, - ): PopupResponse = popupService.postPopup(body).let(::PopupResponse) + ): PopupResponse { + val popup = popupService.postPopup(body) + val imageUri = storageService.getFileUri(popup.imageOriginUri) + return PopupResponse(popup, imageUri) + } @DeleteMapping("/popups/{id}") suspend fun deletePopup( diff --git a/api/src/main/kotlin/controller/PopupController.kt b/api/src/main/kotlin/controller/PopupController.kt index 85b082d0..415125e5 100644 --- a/api/src/main/kotlin/controller/PopupController.kt +++ b/api/src/main/kotlin/controller/PopupController.kt @@ -2,6 +2,7 @@ package com.wafflestudio.snutt.controller import com.wafflestudio.snutt.common.client.ClientInfo import com.wafflestudio.snutt.common.dto.ListResponse +import com.wafflestudio.snutt.common.storage.StorageService import com.wafflestudio.snutt.filter.SnuttNoAuthApiFilterTarget import com.wafflestudio.snutt.popup.dto.PopupResponse import com.wafflestudio.snutt.popup.service.PopupService @@ -20,14 +21,18 @@ import org.springframework.web.bind.annotation.RestController ) class PopupController( private val popupService: PopupService, + private val storageService: StorageService, ) { @GetMapping("") suspend fun getPopups( @RequestAttribute("clientInfo") clientInfo: ClientInfo, ): ListResponse { val popups = popupService.getPopups(clientInfo) + val imageUris = storageService.getFileUris(popups.map { it.imageOriginUri }) + val responses = popups.zip(imageUris) { popup, imageUri -> PopupResponse(popup, imageUri) } + return ListResponse( - content = popups.map(::PopupResponse), + content = responses, totalCount = popups.size, ) } diff --git a/core/src/main/kotlin/common/extension/OciAsyncExtensions.kt b/core/src/main/kotlin/common/extension/OciAsyncExtensions.kt new file mode 100644 index 00000000..82a07ce0 --- /dev/null +++ b/core/src/main/kotlin/common/extension/OciAsyncExtensions.kt @@ -0,0 +1,34 @@ +package com.wafflestudio.snutt.common.extension + +import com.oracle.bmc.responses.AsyncHandler +import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.suspendCancellableCoroutine +import java.util.concurrent.Future +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +internal suspend fun awaitOciCall(execute: (AsyncHandler) -> Future): RESPONSE = + suspendCancellableCoroutine { continuation -> + val future = execute(OciAsyncHandler(continuation)) + continuation.invokeOnCancellation { + future.cancel(true) + } + } + +private class OciAsyncHandler( + private val continuation: CancellableContinuation, +) : AsyncHandler { + override fun onSuccess( + request: REQUEST, + response: RESPONSE, + ) { + continuation.resume(response) + } + + override fun onError( + request: REQUEST, + error: Throwable, + ) { + continuation.resumeWithException(error) + } +} diff --git a/core/src/main/kotlin/common/mail/MailClient.kt b/core/src/main/kotlin/common/mail/MailClient.kt index 0e6f273c..4527b022 100644 --- a/core/src/main/kotlin/common/mail/MailClient.kt +++ b/core/src/main/kotlin/common/mail/MailClient.kt @@ -1,15 +1,14 @@ package com.wafflestudio.snutt.common.mail import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider -import com.oracle.bmc.emaildataplane.EmailDPClient +import com.oracle.bmc.emaildataplane.EmailDPAsyncClient import com.oracle.bmc.emaildataplane.model.EmailAddress import com.oracle.bmc.emaildataplane.model.Recipients import com.oracle.bmc.emaildataplane.model.Sender import com.oracle.bmc.emaildataplane.model.SubmitEmailDetails import com.oracle.bmc.emaildataplane.requests.SubmitEmailRequest +import com.wafflestudio.snutt.common.extension.awaitOciCall import com.wafflestudio.snutt.config.OciConfig -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext import org.springframework.stereotype.Component @Component @@ -18,7 +17,7 @@ class MailClient( ) { private val compartmentId = "ocid1.compartment.oc1..aaaaaaaaxzo4fga6br76o3e34rshtsl6alzripmgdh7f4lg4u4tzezosypaq" private val emailClient = - EmailDPClient + EmailDPAsyncClient .builder() .region(OciConfig.REGION) .build(authProvider) @@ -30,42 +29,41 @@ class MailClient( subject: String, body: String, ) { - withContext(Dispatchers.IO) { - val emailDetails = - SubmitEmailDetails - .builder() - .sender( - Sender - .builder() - .senderAddress( + val emailDetails = + SubmitEmailDetails + .builder() + .sender( + Sender + .builder() + .senderAddress( + EmailAddress + .builder() + .email(sourceEmail) + .name("SNUTT") + .build(), + ).compartmentId(compartmentId) + .build(), + ).recipients( + Recipients + .builder() + .to( + listOf( EmailAddress .builder() - .email(sourceEmail) - .name("SNUTT") + .email(to) .build(), - ).compartmentId(compartmentId) - .build(), - ).recipients( - Recipients - .builder() - .to( - listOf( - EmailAddress - .builder() - .email(to) - .build(), - ), - ).build(), - ).subject(subject) - .bodyHtml(body) - .build() + ), + ).build(), + ).subject(subject) + .bodyHtml(body) + .build() - emailClient.submitEmail( - SubmitEmailRequest - .builder() - .submitEmailDetails(emailDetails) - .build(), - ) - } + val request = + SubmitEmailRequest + .builder() + .submitEmailDetails(emailDetails) + .build() + + awaitOciCall { handler -> emailClient.submitEmail(request, handler) } } } diff --git a/core/src/main/kotlin/common/storage/StorageClient.kt b/core/src/main/kotlin/common/storage/StorageClient.kt index d088d922..ff993ac2 100644 --- a/core/src/main/kotlin/common/storage/StorageClient.kt +++ b/core/src/main/kotlin/common/storage/StorageClient.kt @@ -1,14 +1,15 @@ package com.wafflestudio.snutt.common.storage import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider -import com.oracle.bmc.objectstorage.ObjectStorageClient +import com.oracle.bmc.objectstorage.ObjectStorageAsyncClient import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails import com.oracle.bmc.objectstorage.model.CreatePreauthenticatedRequestDetails.AccessType import com.oracle.bmc.objectstorage.requests.CreatePreauthenticatedRequestRequest import com.oracle.bmc.objectstorage.requests.GetNamespaceRequest +import com.wafflestudio.snutt.common.extension.awaitOciCall import com.wafflestudio.snutt.config.OciConfig -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import jakarta.annotation.PostConstruct +import kotlinx.coroutines.runBlocking import org.springframework.stereotype.Component import java.time.Duration import java.time.Instant @@ -20,92 +21,88 @@ class StorageClient( authProvider: BasicAuthenticationDetailsProvider, ) { private val objectStorageClient = - ObjectStorageClient + ObjectStorageAsyncClient .builder() .region(OciConfig.REGION) .build(authProvider) - private val namespace: String by lazy { - objectStorageClient.getNamespace(GetNamespaceRequest.builder().build()).value - } + private lateinit var namespace: String companion object { - lateinit var instance: StorageClient - private set - private val uploadDuration = Duration.ofMinutes(10) private val getDuration = Duration.ofHours(1) private const val ENDPOINT = "https://objectstorage.ap-chuncheon-1.oraclecloud.com" } - init { - instance = this - } + @PostConstruct + fun initializeNamespace() = + runBlocking { + namespace = fetchNamespace() + } - suspend fun generatePutSignedUris( + suspend fun generatePutSignedUri( storageType: StorageType, key: String, ): String = - withContext(Dispatchers.IO) { - val parDetails = - CreatePreauthenticatedRequestDetails - .builder() - .name("upload-${UUID.randomUUID()}") - .objectName(key) - .accessType(AccessType.ObjectWrite) - .timeExpires(Date.from(Instant.now().plus(uploadDuration))) - .build() - - val response = - objectStorageClient.createPreauthenticatedRequest( - CreatePreauthenticatedRequestRequest - .builder() - .namespaceName(namespace) - .bucketName(storageType.bucketName) - .createPreauthenticatedRequestDetails(parDetails) - .build(), - ) - - "$ENDPOINT${response.preauthenticatedRequest.accessUri}" - } - - fun generateGetUri(originUri: String): String { - val withoutScheme = originUri.substringAfter("s3://") + createSignedUri( + bucketName = storageType.bucketName, + key = key, + accessType = AccessType.ObjectWrite, + expiresIn = uploadDuration, + namePrefix = "upload", + ) + + suspend fun generateGetUri(originUri: String): String { + val withoutScheme = originUri.removePrefix("s3://") val bucketName = withoutScheme.substringBefore("/") - val key = withoutScheme.substringAfter("/") - - if (StorageType.from(bucketName).acl == Acl.PUBLIC_READ) { - return "$ENDPOINT/n/$namespace/b/$bucketName/o/$key" + val key = withoutScheme.substringAfter("/", missingDelimiterValue = "") + + return if (StorageType.from(bucketName).acl == Acl.PUBLIC_READ) { + "$ENDPOINT/n/$namespace/b/$bucketName/o/$key" + } else { + createSignedUri( + bucketName = bucketName, + key = key, + accessType = AccessType.ObjectRead, + expiresIn = getDuration, + namePrefix = "download", + ) } - - return generateGetSignedUri(bucketName, key) } - private fun generateGetSignedUri( + private suspend fun fetchNamespace(): String = + awaitOciCall { handler -> + objectStorageClient.getNamespace(GetNamespaceRequest.builder().build(), handler) + }.value + + private suspend fun createSignedUri( bucketName: String, key: String, + accessType: AccessType, + expiresIn: Duration, + namePrefix: String, ): String { val parDetails = CreatePreauthenticatedRequestDetails .builder() - .name("download-${UUID.randomUUID()}") + .name("$namePrefix-${UUID.randomUUID()}") .objectName(key) - .accessType(AccessType.ObjectRead) - .timeExpires(Date.from(Instant.now().plus(getDuration))) + .accessType(accessType) + .timeExpires(Date.from(Instant.now().plus(expiresIn))) .build() - val response = - objectStorageClient.createPreauthenticatedRequest( - CreatePreauthenticatedRequestRequest - .builder() - .namespaceName(namespace) - .bucketName(bucketName) - .createPreauthenticatedRequestDetails(parDetails) - .build(), - ) + val request = + CreatePreauthenticatedRequestRequest + .builder() + .namespaceName(namespace) + .bucketName(bucketName) + .createPreauthenticatedRequestDetails(parDetails) + .build() + val accessUri = + awaitOciCall { handler -> objectStorageClient.createPreauthenticatedRequest(request, handler) } + .preauthenticatedRequest + .accessUri - return "$ENDPOINT${response.preauthenticatedRequest.accessUri}" + return "$ENDPOINT$accessUri" } } - -fun String.toGetUri(): String = StorageClient.instance.generateGetUri(this) diff --git a/core/src/main/kotlin/common/storage/StorageService.kt b/core/src/main/kotlin/common/storage/StorageService.kt index ed323646..df966de2 100644 --- a/core/src/main/kotlin/common/storage/StorageService.kt +++ b/core/src/main/kotlin/common/storage/StorageService.kt @@ -3,6 +3,9 @@ package com.wafflestudio.snutt.common.storage import com.wafflestudio.snutt.common.exception.TooManyFilesException import com.wafflestudio.snutt.common.storage.FileExtension.JPG import com.wafflestudio.snutt.common.storage.dto.FileUploadUriDto +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import org.springframework.stereotype.Service import java.util.UUID @@ -23,18 +26,30 @@ class StorageService( val storageType = storageSource.storageType val path = storageSource.path?.let { "$it/" } ?: "" - return (1..count).map { - val key = "$path${UUID.randomUUID()}.${JPG.value}" - val uploadUri = storageClient.generatePutSignedUris(storageType, key) + return coroutineScope { + (1..count) + .map { + async { + val key = "$path${UUID.randomUUID()}.${JPG.value}" + val fileOriginUri = "s3://${storageType.bucketName}/$key" - val fileOriginUri = "s3://${storageType.bucketName}/$key" - val fileUri = storageClient.generateGetUri(fileOriginUri) + val uploadUriDeferred = async { storageClient.generatePutSignedUri(storageType, key) } + val fileUriDeferred = async { storageClient.generateGetUri(fileOriginUri) } - FileUploadUriDto( - uploadUri = uploadUri, - fileOriginUri = fileOriginUri, - fileUri = fileUri, - ) + FileUploadUriDto( + uploadUri = uploadUriDeferred.await(), + fileOriginUri = fileOriginUri, + fileUri = fileUriDeferred.await(), + ) + } + }.awaitAll() } } + + suspend fun getFileUri(originUri: String): String = storageClient.generateGetUri(originUri) + + suspend fun getFileUris(originUris: List): List = + coroutineScope { + originUris.map { originUri -> async { getFileUri(originUri) } }.awaitAll() + } } diff --git a/core/src/main/kotlin/popup/dto/PopupResponse.kt b/core/src/main/kotlin/popup/dto/PopupResponse.kt index 9e2c022b..3dbcaad3 100644 --- a/core/src/main/kotlin/popup/dto/PopupResponse.kt +++ b/core/src/main/kotlin/popup/dto/PopupResponse.kt @@ -1,6 +1,5 @@ package com.wafflestudio.snutt.popup.dto -import com.wafflestudio.snutt.common.storage.toGetUri import com.wafflestudio.snutt.popup.data.Popup data class PopupResponse( @@ -16,12 +15,15 @@ data class PopupResponse( val hidden_days: Int?, ) -fun PopupResponse(popup: Popup): PopupResponse = +fun PopupResponse( + popup: Popup, + imageUri: String, +): PopupResponse = PopupResponse( id = popup.id!!, key = popup.key, - imageUri = popup.imageOriginUri.toGetUri(), - image_url = popup.imageOriginUri.toGetUri(), + imageUri = imageUri, + image_url = imageUri, linkUrl = popup.linkUrl, hiddenDays = popup.hiddenDays, hidden_days = popup.hiddenDays, From b0d7832da70df7645899ce2e67190991c0b8e58a Mon Sep 17 00:00:00 2001 From: Chanyeong Lim Date: Sat, 28 Feb 2026 22:13:23 +0900 Subject: [PATCH 21/21] remove unneeded mongodb username and password --- core/src/main/resources/application-common.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/main/resources/application-common.yml b/core/src/main/resources/application-common.yml index d42bb1ce..f4e9b75b 100644 --- a/core/src/main/resources/application-common.yml +++ b/core/src/main/resources/application-common.yml @@ -3,8 +3,6 @@ spring: default: local mongodb: uri: - username: - password: data: redis: repositories: