Skip to content

Commit f288e30

Browse files
authored
feat(oss): migrate from Alibaba Cloud OSS to Qiniu Kodo (#19)
* feat(oss): migrate from Alibaba Cloud OSS to Qiniu Kodo * perf(deploy): skip unchanged dependency images in test deploy * ci(deploy-test): switch from SCP zip transfer to Alibaba Cloud ACR - Push cixing-api image + 5 base images to ACR (Hangzhou registry) - ECS pulls images from ACR via docker compose pull instead of gzip load - Remove docker save/gzip/scp image transfer steps - Remove /opt/cixing/images/ tarball cleanup - Update docker-compose.prod.yml to reference ACR image paths - Add ACR_USERNAME / ACR_PASSWORD to required secrets validation * ci(deploy-test): fix docker login host error * ci(deploy-test): push cixing-api image to Alibaba Cloud ACR - Push image to registry.cn-hangzhou.aliyuncs.com/muxi/memento:<sha> - Remove docker save/gzip/scp image transfer steps - ECS pulls cixing-api from ACR, base images from Docker Hub - Add ACR_USERNAME / ACR_PASSWORD to required secrets validation - Note: CONFIG must also fix postgres DSN from 127.0.0.1 to postgres * fix(deploy-test): pass ACR credentials via SSH environment * ci(deploy-test): pull only cixing-api from ACR * fix(oss): remove duplicate https:// prefix in qiniu upload host * fix(oss): remove null * fix(oss): use SrcUpHosts (upload domain) instead of GetIoHost (IO domain) for presign upload URL * fix(oss): presign returns FormData token field instead of Authorization header for Qiniu Kodo - storage.go: PresignPut uses FormFields { token: upToken } instead of Authorization header - object_storage.go: PresignResult gains FormFields map - OpenAPI spec: UploadPresignedTarget gains form_fields field, method enum adds POST - Handlers: map FormFields through to API response - Qiniu form upload requires multipart/form-data with token in the form body * fix(ci): remove duplicate Configure SSH block and nginx references * fix(oss): gofmt client.go struct field alignment * chore(test): remove tests that reference removed OSS config fields * fix(oss): remove unused functions and fix errcheck lint warning * fix: remove unused functions, fix integration test for Qiniu URL style * fix(ci): resolve lint errors and integration test failures after Qiniu migration
1 parent e22566a commit f288e30

29 files changed

Lines changed: 779 additions & 965 deletions

.github/workflows/deploy-test.yml

Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: deploy-test
22

33
on:
44
workflow_dispatch:
5-
# push:
6-
# branches:
7-
# - develop
5+
push:
6+
branches:
7+
- develop
88

99
permissions:
1010
contents: read
@@ -14,10 +14,7 @@ concurrency:
1414
cancel-in-progress: false
1515

1616
env:
17-
API_IMAGE_REPOSITORY: cixing-api
18-
POSTGRES_IMAGE: postgres:16-alpine
19-
REDIS_IMAGE: redis:7-alpine
20-
MAILPIT_IMAGE: axllent/mailpit:v1.29.7
17+
ACR_REPO: registry.cn-hangzhou.aliyuncs.com/muxi/memento
2118

2219
jobs:
2320
deploy:
@@ -34,36 +31,52 @@ jobs:
3431
TEST_SERVER_SSH_KEY: ${{ secrets.TEST_SERVER_SSH_KEY }}
3532
TEST_CONFIG_YAML: ${{ secrets.TEST_CONFIG_YAML }}
3633
TEST_POSTGRES_PASSWORD: ${{ secrets.TEST_POSTGRES_PASSWORD }}
34+
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
35+
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
3736
run: |
3837
set -eu
3938
test -n "$TEST_SERVER_HOST"
4039
test -n "$TEST_SERVER_USERNAME"
4140
test -n "$TEST_SERVER_SSH_KEY"
4241
test -n "$TEST_CONFIG_YAML"
4342
test -n "$TEST_POSTGRES_PASSWORD"
43+
test -n "$ACR_USERNAME"
44+
test -n "$ACR_PASSWORD"
4445
45-
- name: Build API image
46+
- name: Login to ACR
47+
env:
48+
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
49+
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
4650
run: |
47-
set -eu
48-
IMAGE_LOCAL="${API_IMAGE_REPOSITORY}:${GITHUB_SHA}"
49-
docker build --platform linux/amd64 -f deploy/docker/Dockerfile.api -t "$IMAGE_LOCAL" .
50-
echo "CIXING_IMAGE=$IMAGE_LOCAL" >> "$GITHUB_ENV"
51+
echo "$ACR_PASSWORD" | docker login --username "$ACR_USERNAME" --password-stdin registry.cn-hangzhou.aliyuncs.com
5152
52-
- name: Pull dependency images
53+
- name: Build and push cixing-api to ACR
5354
run: |
5455
set -eu
55-
docker pull --platform linux/amd64 "$POSTGRES_IMAGE"
56-
docker pull --platform linux/amd64 "$REDIS_IMAGE"
57-
docker pull --platform linux/amd64 "$MAILPIT_IMAGE"
56+
IMAGE_TAG="${GITHUB_SHA}"
57+
IMAGE_ACR="${ACR_REPO}:${IMAGE_TAG}"
58+
docker build --platform linux/amd64 -f deploy/docker/Dockerfile.api -t "cixing-api:${IMAGE_TAG}" -t "$IMAGE_ACR" .
59+
docker push "$IMAGE_ACR"
5860
59-
- name: Save deployment images
61+
- name: Mirror base images to ACR (skip if tag exists)
6062
run: |
6163
set -eu
62-
mkdir -p /tmp/cixing-deploy/images
63-
docker save "$CIXING_IMAGE" | gzip > /tmp/cixing-deploy/images/cixing-api.tar.gz
64-
docker save "$POSTGRES_IMAGE" | gzip > /tmp/cixing-deploy/images/postgres.tar.gz
65-
docker save "$REDIS_IMAGE" | gzip > /tmp/cixing-deploy/images/redis.tar.gz
66-
docker save "$MAILPIT_IMAGE" | gzip > /tmp/cixing-deploy/images/mailpit.tar.gz
64+
mirror() {
65+
local source="$1"
66+
local name="$2"
67+
local acr_tag="${ACR_REGISTRY}/${name}"
68+
if docker manifest inspect "$acr_tag" >/dev/null 2>&1; then
69+
echo "skip $name: already in ACR"
70+
return
71+
fi
72+
echo "mirroring $source -> $acr_tag"
73+
docker pull --platform linux/amd64 "$source"
74+
docker tag "$source" "$acr_tag"
75+
docker push "$acr_tag"
76+
}
77+
mirror "${POSTGRES_IMAGE:-postgres:16-alpine}" postgres
78+
mirror "${REDIS_IMAGE:-redis:7-alpine}" redis
79+
mirror "${MAILPIT_IMAGE:-axllent/mailpit:v1.29.7}" mailpit
6780
6881
- name: Prepare deployment files
6982
env:
@@ -75,14 +88,13 @@ jobs:
7588
cp deploy/compose/docker-compose.prod.yml /tmp/cixing-deploy/docker-compose.yml
7689
printf '%s' "$TEST_CONFIG_YAML" > /tmp/cixing-deploy/config.yaml
7790
{
78-
echo "CIXING_IMAGE=${CIXING_IMAGE}"
79-
echo "POSTGRES_IMAGE=${POSTGRES_IMAGE}"
80-
echo "REDIS_IMAGE=${REDIS_IMAGE}"
81-
echo "MAILPIT_IMAGE=${MAILPIT_IMAGE}"
91+
echo "CIXING_IMAGE=${ACR_REPO}:${GITHUB_SHA}"
92+
echo "POSTGRES_IMAGE=postgres:16-alpine"
93+
echo "REDIS_IMAGE=redis:7-alpine"
94+
echo "MAILPIT_IMAGE=axllent/mailpit:v1.29.7"
8295
echo "POSTGRES_PASSWORD=${TEST_POSTGRES_PASSWORD}"
8396
echo "API_PORT=8080"
8497
echo "MAILPIT_UI_PORT=8025"
85-
echo "DOMAIN=test.memento.muxixyz.com"
8698
} > /tmp/cixing-deploy/deploy.env
8799
88100
- name: Configure SSH
@@ -96,36 +108,33 @@ jobs:
96108
chmod 600 ~/.ssh/id_test_server
97109
ssh-keyscan -H "$TEST_SERVER_HOST" >> ~/.ssh/known_hosts
98110
99-
- name: Upload deployment assets
111+
- name: Upload deployment files
100112
env:
101113
TEST_SERVER_HOST: ${{ secrets.TEST_SERVER_HOST }}
102114
TEST_SERVER_USERNAME: ${{ secrets.TEST_SERVER_USERNAME }}
103115
run: |
104116
set -eu
117+
SCP_OPTS="-o ConnectTimeout=10 -o ServerAliveInterval=15 -i ~/.ssh/id_test_server"
118+
105119
ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" \
106-
"sudo mkdir -p /opt/cixing/config /opt/cixing/images && sudo chown -R $TEST_SERVER_USERNAME /opt/cixing"
107-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/docker-compose.yml \
120+
"sudo mkdir -p /opt/cixing/config && sudo chown -R $TEST_SERVER_USERNAME /opt/cixing"
121+
122+
scp $SCP_OPTS /tmp/cixing-deploy/docker-compose.yml \
108123
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/docker-compose.yml"
109-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/deploy.env \
124+
scp $SCP_OPTS /tmp/cixing-deploy/deploy.env \
110125
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/deploy.env"
111-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/config.yaml \
126+
scp $SCP_OPTS /tmp/cixing-deploy/config.yaml \
112127
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/config/config.yaml"
113-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/cixing-api.tar.gz \
114-
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/cixing-api.tar.gz"
115-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/postgres.tar.gz \
116-
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/postgres.tar.gz"
117-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/redis.tar.gz \
118-
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/redis.tar.gz"
119-
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/mailpit.tar.gz \
120-
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/mailpit.tar.gz"
121128
122129
- name: Deploy on ECS
123130
env:
124131
TEST_SERVER_HOST: ${{ secrets.TEST_SERVER_HOST }}
125132
TEST_SERVER_USERNAME: ${{ secrets.TEST_SERVER_USERNAME }}
133+
ACR_USERNAME: ${{ secrets.ACR_USERNAME }}
134+
ACR_PASSWORD: ${{ secrets.ACR_PASSWORD }}
126135
run: |
127136
set -eu
128-
ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" <<'REMOTE'
137+
ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" "ACR_USERNAME='${ACR_USERNAME}' ACR_PASSWORD='${ACR_PASSWORD}' /bin/sh -s" <<'REMOTE'
129138
set -eu
130139
cd /opt/cixing
131140
DOCKER="docker"
@@ -158,14 +167,13 @@ jobs:
158167
compose ps
159168
compose logs --tail=200 api
160169
}
161-
cleanup_deployment_artifacts() {
162-
echo "cleaning deployment artifacts"
163-
rm -f /opt/cixing/images/*.tar.gz || true
164-
current_image="$(awk -F= '$1=="CIXING_IMAGE"{print $2}' /opt/cixing/deploy.env)"
165-
if [ -z "$current_image" ]; then
170+
cleanup_old_images() {
171+
echo "cleaning old cixing-api images"
172+
current_tag="$(awk -F= '$1=="CIXING_IMAGE"{print $2}' /opt/cixing/deploy.env | sed 's|.*:||')"
173+
if [ -z "$current_tag" ]; then
166174
return 0
167175
fi
168-
for image_ref in $($DOCKER images --format '{{.Repository}}:{{.Tag}}' | grep '^cixing-api:' | grep -F -x -v "$current_image" || true); do
176+
for image_ref in $($DOCKER images --format '{{.Repository}}:{{.Tag}}' | grep 'cixing\|memento' | grep -v "$current_tag" || true); do
169177
$DOCKER rmi "$image_ref" >/dev/null 2>&1 || true
170178
done
171179
}
@@ -200,10 +208,14 @@ jobs:
200208
chmod 600 /opt/cixing/deploy.env
201209
chmod 644 /opt/cixing/config/config.yaml
202210
cleanup_existing_compose_state
203-
gzip -dc /opt/cixing/images/cixing-api.tar.gz | $DOCKER load
204-
gzip -dc /opt/cixing/images/postgres.tar.gz | $DOCKER load
205-
gzip -dc /opt/cixing/images/redis.tar.gz | $DOCKER load
206-
gzip -dc /opt/cixing/images/mailpit.tar.gz | $DOCKER load
211+
212+
echo "pulling images from ACR"
213+
# Only pull cixing-api from ACR (base images already exist on host)
214+
CIXING_IMAGE="$(grep '^CIXING_IMAGE=' /opt/cixing/deploy.env | cut -d= -f2-)"
215+
echo "$ACR_PASSWORD" | $DOCKER login --username "$ACR_USERNAME" --password-stdin registry.cn-hangzhou.aliyuncs.com
216+
echo "pulling $CIXING_IMAGE"
217+
$DOCKER pull "$CIXING_IMAGE"
218+
207219
echo "starting dependency services"
208220
compose up -d postgres redis mailpit
209221
echo "running database migrations"
@@ -264,7 +276,7 @@ jobs:
264276
final_api_state="$($DOCKER inspect --format '{{.State.Status}}' "$final_api_container_id")"
265277
final_api_health="$($DOCKER inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$final_api_container_id")"
266278
echo "final api status: container=$final_api_container_id state=$final_api_state health=$final_api_health"
267-
cleanup_deployment_artifacts
279+
cleanup_old_images
268280
compose ps
269281
compose logs --tail=80 api
270282
REMOTE

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,6 @@ Thumbs.db
3434
.idea/
3535
.vscode/
3636
/docs/
37+
38+
# Claude Code
39+
CLAUDE.md

api/openapi/openapi.v1.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ components:
528528
properties:
529529
method:
530530
type: string
531-
enum: [PUT]
531+
enum: [POST, PUT]
532532
url:
533533
type: string
534534
format: uri
@@ -537,6 +537,11 @@ components:
537537
additionalProperties:
538538
type: string
539539
nullable: true
540+
form_fields:
541+
type: object
542+
additionalProperties:
543+
type: string
544+
nullable: true
540545
object_key:
541546
type: string
542547
minLength: 1

config.yaml

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,19 @@ redis:
1919
db: 0
2020

2121
oss:
22-
credential_mode: "static"
23-
ecs_role_name: "CixingEcsOssUploadRole"
24-
disable_imdsv1: true
25-
assume_role_arn: ""
26-
assume_role_session_name: ""
27-
assume_role_external_id: ""
28-
assume_role_sts_endpoint: ""
29-
assume_role_session_duration: 1h
30-
bucket: "change-me"
31-
region: "cn-hongkong"
22+
bucket: "memento"
23+
region: "" # 七牛云区域标识,可选。留空由 SDK 自动查询
3224
access_key_id: ""
3325
access_key_secret: ""
34-
public_endpoint: ""
35-
public_endpoint_is_cname: false
36-
internal_endpoint: ""
37-
internal_endpoint_is_cname: false
38-
use_internal_endpoint: false
26+
public_endpoint: "https://memento.muxixyz.com" # 七牛云 CDN 加速域名
3927
put_presign_expire: 15m
4028
get_presign_expire: 10m
4129
upload_prefix: "uploads/"
4230
styles:
43-
card_4x3: "card_4x3"
44-
square_small: "square_small"
45-
square_medium: "square_medium"
46-
detail_large: "detail_large"
31+
card_4x3: "imageView2/1/w/900/h/1200|imageMogr2/auto-orient/quality/85/format/webp"
32+
square_small: "imageView2/1/w/320/h/320|imageMogr2/auto-orient/quality/82/format/webp"
33+
square_medium: "imageView2/1/w/640/h/640|imageMogr2/auto-orient/quality/85/format/webp"
34+
detail_large: "imageMogr2/auto-orient/quality/88/format/webp"
4735

4836
jwt:
4937
private_key_pem: |-

configs/config.local.yaml.example

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,33 +21,19 @@ redis:
2121
db: 0
2222

2323
oss:
24-
credential_mode: "static"
25-
ecs_role_name: "CixingEcsOssUploadRole"
26-
disable_imdsv1: true
27-
# For cross-account ECS RAM role access, set credential_mode to
28-
# "ecs_ram_role_assume_role" and fill the target account role ARN below.
29-
assume_role_arn: ""
30-
assume_role_session_name: ""
31-
assume_role_external_id: ""
32-
assume_role_sts_endpoint: ""
33-
assume_role_session_duration: 1h
34-
bucket: "change-me"
35-
region: "cn-hongkong"
24+
bucket: "memento"
25+
region: "" # 本地开发可不填,SDK 自动查询
3626
access_key_id: ""
3727
access_key_secret: ""
38-
public_endpoint: ""
39-
public_endpoint_is_cname: false
40-
internal_endpoint: ""
41-
internal_endpoint_is_cname: false
42-
use_internal_endpoint: false
28+
public_endpoint: "https://memento.muxixyz.com" # 七牛云 CDN 加速域名
4329
put_presign_expire: 15m
4430
get_presign_expire: 10m
45-
upload_prefix: "uploads/"
31+
upload_prefix: "test/" # 本地开发用 test/ 前缀隔离
4632
styles:
47-
card_4x3: "card_4x3"
48-
square_small: "square_small"
49-
square_medium: "square_medium"
50-
detail_large: "detail_large"
33+
card_4x3: "imageView2/1/w/900/h/1200|imageMogr2/auto-orient/quality/85/format/webp"
34+
square_small: "imageView2/1/w/320/h/320|imageMogr2/auto-orient/quality/82/format/webp"
35+
square_medium: "imageView2/1/w/640/h/640|imageMogr2/auto-orient/quality/85/format/webp"
36+
detail_large: "imageMogr2/auto-orient/quality/88/format/webp"
5137

5238
jwt:
5339
private_key_pem: |-

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ module cixing
33
go 1.25.1
44

55
require (
6-
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.4.0
7-
github.com/aliyun/credentials-go v1.4.8
86
github.com/getkin/kin-openapi v0.133.0
97
github.com/gin-gonic/gin v1.11.0
108
github.com/go-playground/validator/v10 v10.27.0
@@ -15,6 +13,7 @@ require (
1513
github.com/oapi-codegen/gin-middleware v1.0.2
1614
github.com/oapi-codegen/oapi-codegen/v2 v2.5.1
1715
github.com/oapi-codegen/runtime v1.1.2
16+
github.com/qiniu/go-sdk/v7 v7.26.12
1817
github.com/redis/go-redis/v9 v9.17.3
1918
github.com/sqlc-dev/sqlc v1.30.0
2019
golang.org/x/crypto v0.48.0
@@ -44,13 +43,13 @@ require (
4443
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
4544
github.com/Azure/go-autorest/logger v0.2.1 // indirect
4645
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
46+
github.com/BurntSushi/toml v1.3.2 // indirect
4747
github.com/ClickHouse/clickhouse-go v1.4.3 // indirect
4848
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
4949
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
5050
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0 // indirect
5151
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0 // indirect
52-
github.com/alibabacloud-go/debug v1.0.1 // indirect
53-
github.com/alibabacloud-go/tea v1.2.2 // indirect
52+
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
5453
github.com/andybalholm/brotli v1.0.5 // indirect
5554
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
5655
github.com/apache/arrow/go/v10 v10.0.1 // indirect
@@ -94,6 +93,7 @@ require (
9493
github.com/felixge/httpsnoop v1.0.4 // indirect
9594
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
9695
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
96+
github.com/gammazero/toposort v0.1.1 // indirect
9797
github.com/gin-contrib/sse v1.1.0 // indirect
9898
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
9999
github.com/go-logr/logr v1.4.3 // indirect
@@ -108,6 +108,7 @@ require (
108108
github.com/goccy/go-yaml v1.18.0 // indirect
109109
github.com/gocql/gocql v0.0.0-20210515062232-b7ef815b4556 // indirect
110110
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
111+
github.com/gofrs/flock v0.8.1 // indirect
111112
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
112113
github.com/golang-sql/sqlexp v0.1.0 // indirect
113114
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
@@ -238,7 +239,6 @@ require (
238239
google.golang.org/grpc v1.75.0 // indirect
239240
google.golang.org/protobuf v1.36.9 // indirect
240241
gopkg.in/inf.v0 v0.9.1 // indirect
241-
gopkg.in/ini.v1 v1.67.0 // indirect
242242
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
243243
gopkg.in/yaml.v2 v2.4.0 // indirect
244244
modernc.org/b v1.0.0 // indirect

0 commit comments

Comments
 (0)