Author SHA1 Message Date
Codex Local 8955b57ddc fix: allow disabling trace export
ci / verify (push) Canceled after 0s
2026-08-26 10:51:33 +08:00
Codex Local f3dc6deb2e fix: separate public object store TLS 2026-08-26 10:26:06 +08:00
Codex Local 42e7a82dad chore: keep systemd units in LF 2026-08-26 10:06:06 +08:00
Codex Local ea90cac50c deploy: enable trusted HTTPS IP endpoint 2026-08-26 10:04:01 +08:00
Codex Local c3975babf1 fix: seed cloud OIDC issuer 2026-08-26 09:35:38 +08:00
Codex Local 7b32b479f5 deploy: add single-host cloud profile 2026-08-26 09:00:07 +08:00
Codex Local 43239d38f9 fix: allow loopback OIDC callbacks 2026-08-26 07:46:41 +08:00
15 changed files with 400 additions and 13 deletions
+3
View File
@@ -16,6 +16,9 @@
*.md text eol=lf *.md text eol=lf
*.html text eol=lf *.html text eol=lf
*.css text eol=lf *.css text eol=lf
*.conf text eol=lf
*.service text eol=lf
*.timer text eol=lf
*.js text eol=lf *.js text eol=lf
*.mjs text eol=lf *.mjs text eol=lf
*.cjs text eol=lf *.cjs text eol=lf
+1
View File
@@ -9,6 +9,7 @@ coverage*.out
/coverage /coverage
*.exe *.exe
/opsconfig /opsconfig
/deploy/server/.env
bin/ bin/
dist/ dist/
tmp/ tmp/
+2
View File
@@ -101,6 +101,8 @@ Set-Location E:\Codex\story\platform
- 模拟语音供应商存活检查:<http://localhost:59200/health/live> - 模拟语音供应商存活检查:<http://localhost:59200/health/live>
- 模拟告警接收器存活检查:<http://localhost:59210/health/live> - 模拟告警接收器存活检查:<http://localhost:59210/health/live>
Web也允许通过`http://127.0.0.1:3100`访问;本地Keycloak客户端为`localhost`和`127.0.0.1`分别登记了3100生产预览端口与5173前端开发端口,登录回调必须保持当前浏览器来源一致。
本地Web点击“登录研发环境”后可使用`owner`、`editor`或`outsider`三个合成账号,密码均为`LocalOnly123!`。账号只用于本机联调,权限说明见[家庭角色权限矩阵](docs/permission-matrix.md)。 本地Web点击“登录研发环境”后可使用`owner`、`editor`或`outsider`三个合成账号,密码均为`LocalOnly123!`。账号只用于本机联调,权限说明见[家庭角色权限矩阵](docs/permission-matrix.md)。
Web镜像在容器启动时根据`WEB_OIDC_AUTHORITY`和`WEB_OIDC_CLIENT_ID`生成不缓存的`runtime-config.js`。dev、preprod和prod可复用同一镜像,只需为各环境设置对应OIDC地址和客户端编号;这些值是浏览器可见的公开配置,不能用于保存客户端密钥。 Web镜像在容器启动时根据`WEB_OIDC_AUTHORITY`和`WEB_OIDC_CLIENT_ID`生成不缓存的`runtime-config.js`。dev、preprod和prod可复用同一镜像,只需为各环境设置对应OIDC地址和客户端编号;这些值是浏览器可见的公开配置,不能用于保存客户端密钥。
+18 -2
View File
@@ -15,8 +15,24 @@
"publicClient": true, "publicClient": true,
"standardFlowEnabled": true, "standardFlowEnabled": true,
"directAccessGrantsEnabled": true, "directAccessGrantsEnabled": true,
"redirectUris": ["http://localhost:3100/*", "http://localhost:5173/*"], "redirectUris": [
"webOrigins": ["http://localhost:3100", "http://localhost:5173"], "http://localhost:3100/*",
"http://127.0.0.1:3100/*",
"http://121.41.46.238/*",
"https://121.41.46.238/*",
"https://121.41.46.238:80/*",
"http://localhost:5173/*",
"http://127.0.0.1:5173/*"
],
"webOrigins": [
"http://localhost:3100",
"http://127.0.0.1:3100",
"http://121.41.46.238",
"https://121.41.46.238",
"https://121.41.46.238:80",
"http://localhost:5173",
"http://127.0.0.1:5173"
],
"protocol": "openid-connect", "protocol": "openid-connect",
"protocolMappers": [ "protocolMappers": [
{ {
+24
View File
@@ -0,0 +1,24 @@
# 阿里云单机演示部署
该配置用于单机公网演示,不是生产验收配置。只向公网发布HTTP 80和HTTPS 443;PostgreSQL、Redis、MinIO、Keycloak内部端口以及API/Worker端口均不映射到宿主机。当前阿里云演示机使用Let's Encrypt短期IP证书,`certbot-ip-renew.timer`负责自动续期。真实RTC、模型、内容安全、云KMS、备份恢复和监控告警仍按阶段8验收清单执行。
服务器至少需要Docker Engine 29与Compose 2.40。首次部署先生成权限为600的服务器专用配置:
```sh
./deploy/server/bootstrap-secrets.sh \
https://SERVER_IP \
0.64.0-rc.cloud1 \
FULL_GIT_COMMIT \
2026-08-26T00:00:00Z
```
载入同一候选版本的`data-terminal-cloud-backend`和`data-terminal-cloud-web`镜像后执行:
```sh
docker compose -f compose.yaml -f deploy/server/compose.override.yaml \
--env-file deploy/server/.env up -d --no-build \
postgres redis minio keycloak migrate seed mock-alert-receiver callbacks \
mock-provider api worker web gateway
```
安全组和主机防火墙只保留TCP 80、443以及受限来源的SSH 22。如果云安全组暂未放行443,可临时将宿主机80映射到网关443,并使用`https://SERVER_IP:80`;放行443后应恢复标准HTTPS地址。正式环境仍应配置自有域名、生产OIDC和真实外部服务,并切换到Kubernetes生产覆盖层。
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env sh
set -eu
if [ "$#" -ne 4 ]; then
echo "usage: $0 PUBLIC_ORIGIN VERSION COMMIT BUILD_TIME" >&2
exit 2
fi
public_origin=$1
build_version=$2
build_commit=$3
build_time=$4
public_host=${public_origin#http://}
public_host=${public_host#https://}
output_path=$(dirname "$0")/.env
if [ -e "$output_path" ]; then
echo "refusing to overwrite existing $output_path" >&2
exit 1
fi
umask 077
{
printf 'PUBLIC_ORIGIN=%s\n' "$public_origin"
printf 'PUBLIC_HOST=%s\n' "$public_host"
printf 'PLATFORM_BUILD_VERSION=%s\n' "$build_version"
printf 'PLATFORM_BUILD_COMMIT=%s\n' "$build_commit"
printf 'PLATFORM_BUILD_TIME=%s\n' "$build_time"
printf 'POSTGRES_PASSWORD=%s\n' "$(openssl rand -hex 24)"
printf 'REDIS_PASSWORD=%s\n' "$(openssl rand -hex 24)"
printf 'MINIO_ACCESS_KEY=%s\n' "$(openssl rand -hex 12)"
printf 'MINIO_SECRET_KEY=%s\n' "$(openssl rand -hex 32)"
printf 'KEYCLOAK_ADMIN_USERNAME=platform-admin\n'
printf 'KEYCLOAK_ADMIN_PASSWORD=%s\n' "$(openssl rand -hex 24)"
printf 'DEVICE_LOCAL_SECRET=%s\n' "$(openssl rand -hex 32)"
printf 'RTC_TOKEN_SIGNING_SECRET=%s\n' "$(openssl rand -hex 32)"
printf 'CURSOR_SIGNING_SECRET=%s\n' "$(openssl rand -hex 32)"
printf 'SHARE_TOKEN_SIGNING_SECRET=%s\n' "$(openssl rand -hex 32)"
printf 'PROVIDER_CALLBACK_SECRET=%s\n' "$(openssl rand -hex 32)"
printf 'MOCK_PROVIDER_API_KEY=%s\n' "$(openssl rand -hex 24)"
printf 'ALERT_WEBHOOK_SECRET=%s\n' "$(openssl rand -hex 32)"
} > "$output_path"
chmod 600 "$output_path"
echo "created $output_path with mode 600"
+10
View File
@@ -0,0 +1,10 @@
[Unit]
Description=Renew the data terminal cloud IP certificate
After=docker.service network-online.target
Requires=docker.service
[Service]
Type=oneshot
WorkingDirectory=/opt/data-terminal-cloud/current
ExecStart=/opt/certbot-5-runtime/bin/certbot renew --cert-name 121.41.46.238 --quiet --pre-hook "/usr/bin/docker compose -f compose.yaml -f deploy/server/compose.override.yaml --env-file deploy/server/.env stop gateway" --post-hook "/usr/bin/docker compose -f compose.yaml -f deploy/server/compose.override.yaml --env-file deploy/server/.env start gateway"
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=Check the data terminal cloud IP certificate twice daily
[Timer]
OnCalendar=*-*-* 03,15:17:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
+176
View File
@@ -0,0 +1,176 @@
x-server-backend-environment: &server-backend-environment
APP_ENV: local
LOG_LEVEL: info
DATABASE_URL: postgres://voice:${POSTGRES_PASSWORD}@postgres:5432/voice_platform?sslmode=disable
REDIS_ADDR: redis:6379
REDIS_PASSWORD: ${REDIS_PASSWORD}
REDIS_DB: "0"
AUTH_ISSUER_URL: ${PUBLIC_ORIGIN}/realms/voice-local
AUTH_JWKS_URL: http://keycloak:8080/realms/voice-local/protocol/openid-connect/certs
AUTH_AUDIENCE: voice-platform-web
OTEL_EXPORTER_OTLP_ENDPOINT: ""
OBJECT_STORE_ENDPOINT: minio:9000
OBJECT_STORE_PUBLIC_ENDPOINT: ${PUBLIC_HOST}
OBJECT_STORE_ACCESS_KEY: ${MINIO_ACCESS_KEY}
OBJECT_STORE_SECRET_KEY: ${MINIO_SECRET_KEY}
OBJECT_STORE_EXPORT_BUCKET: private-exports
OBJECT_STORE_REGION: us-east-1
OBJECT_STORE_SECURE: "false"
OBJECT_STORE_PUBLIC_SECURE: "true"
CURSOR_SIGNING_SECRET: ${CURSOR_SIGNING_SECRET}
SHARE_TOKEN_SIGNING_SECRET: ${SHARE_TOKEN_SIGNING_SECRET}
EMBEDDING_PROVIDER: local
VOICE_PROVIDER: mock
services:
otel-collector:
ports: !reset []
prometheus:
ports: !reset []
grafana:
ports: !reset []
postgres:
ports: !reset []
environment:
POSTGRES_DB: voice_platform
POSTGRES_USER: voice
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
restart: unless-stopped
redis:
ports: !reset []
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD}"]
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
restart: unless-stopped
minio:
ports: !reset []
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
restart: unless-stopped
keycloak:
ports: !reset []
command: ["start-dev", "--import-realm"]
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: ${KEYCLOAK_ADMIN_USERNAME}
KC_BOOTSTRAP_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD}
KC_HOSTNAME: ${PUBLIC_ORIGIN}
KC_HTTP_ENABLED: "true"
KC_PROXY_HEADERS: xforwarded
JAVA_OPTS_KC_HEAP: -Xms128m -Xmx384m
volumes:
- ./deploy/keycloak:/opt/keycloak/data/import:ro
- keycloak-data:/opt/keycloak/data
restart: unless-stopped
migrate:
command:
- -path=/migrations
- -database=postgres://voice:${POSTGRES_PASSWORD}@postgres:5432/voice_platform?sslmode=disable
- up
seed:
environment:
PGPASSWORD: ${POSTGRES_PASSWORD}
command:
- sh
- -ec
- |
sed "s#http://localhost:58180/realms/voice-local#${PUBLIC_ORIGIN}/realms/voice-local#g" /fixtures/local_seed.sql |
psql -h postgres -U voice -d voice_platform -v ON_ERROR_STOP=1
api:
image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION}
pull_policy: never
ports: !reset []
environment:
<<: *server-backend-environment
HTTP_ADDR: 0.0.0.0:8080
DEVICE_LOCAL_SECRET: ${DEVICE_LOCAL_SECRET}
RTC_TOKEN_SIGNING_SECRET: ${RTC_TOKEN_SIGNING_SECRET}
RTC_TOKEN_ISSUER: local
RTC_APP_ID: local-public-rtc-app
restart: unless-stopped
callbacks:
image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION}
pull_policy: never
ports: !reset []
environment:
<<: *server-backend-environment
HTTP_ADDR: 0.0.0.0:8081
PROVIDER_CALLBACK_SECRET: ${PROVIDER_CALLBACK_SECRET}
RATE_LIMIT_RPS: "1000"
RATE_LIMIT_BURST: "1500"
restart: unless-stopped
worker:
image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION}
pull_policy: never
ports: !reset []
environment:
<<: *server-backend-environment
HTTP_ADDR: 0.0.0.0:8082
MOCK_PROVIDER_BASE_URL: http://mock-provider:8083
MOCK_PROVIDER_API_KEY: ${MOCK_PROVIDER_API_KEY}
ALERT_WEBHOOK_URL: http://mock-alert-receiver:8084/webhooks/alerts
ALERT_WEBHOOK_SECRET: ${ALERT_WEBHOOK_SECRET}
CONTENT_GENERATOR: local
CONTENT_SAFETY_PROVIDER: local
restart: unless-stopped
mock-provider:
image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION}
pull_policy: never
ports: !reset []
environment:
HTTP_ADDR: 0.0.0.0:8083
MOCK_PROVIDER_API_KEY: ${MOCK_PROVIDER_API_KEY}
MOCK_PROVIDER_CALLBACK_URL: http://callbacks:8081/callbacks/v1/vertc/voice-chat
PROVIDER_CALLBACK_SECRET: ${PROVIDER_CALLBACK_SECRET}
MOCK_PROVIDER_START_DELAY: 100ms
MOCK_PROVIDER_DUPLICATE_CALLBACKS: "2"
restart: unless-stopped
mock-alert-receiver:
image: data-terminal-cloud-backend:${PLATFORM_BUILD_VERSION}
pull_policy: never
ports: !reset []
environment:
HTTP_ADDR: 0.0.0.0:8084
ALERT_WEBHOOK_SECRET: ${ALERT_WEBHOOK_SECRET}
restart: unless-stopped
web:
image: data-terminal-cloud-web:${PLATFORM_BUILD_VERSION}
pull_policy: never
ports: !reset []
environment:
WEB_OIDC_AUTHORITY: ${PUBLIC_ORIGIN}/realms/voice-local
WEB_OIDC_CLIENT_ID: voice-platform-web
restart: unless-stopped
gateway:
image: nginx:1.30-alpine
ports:
- "80:443"
- "443:443"
volumes:
- ./deploy/server/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- api
- callbacks
- keycloak
- minio
- web
restart: unless-stopped
volumes:
keycloak-data:
+69
View File
@@ -0,0 +1,69 @@
server {
listen 80;
server_name _;
return 301 https://121.41.46.238$request_uri;
}
server {
listen 443 ssl;
server_name 121.41.46.238;
server_tokens off;
client_max_body_size 256m;
ssl_certificate /etc/letsencrypt/live/121.41.46.238/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/121.41.46.238/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=31536000" always;
resolver 127.0.0.11 ipv6=off valid=10s;
set $api_backend http://api:8080;
set $callbacks_backend http://callbacks:8081;
set $keycloak_backend http://keycloak:8080;
set $minio_backend http://minio:9000;
set $web_backend http://web:8080;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /api/ {
proxy_pass $api_backend;
proxy_buffering off;
proxy_read_timeout 3600s;
}
location /device/ {
proxy_pass $api_backend;
proxy_buffering off;
}
location /callbacks/ {
proxy_pass $callbacks_backend;
proxy_buffering off;
}
location /realms/ {
proxy_pass $keycloak_backend;
}
location /resources/ {
proxy_pass $keycloak_backend;
}
location /private-exports/ {
proxy_pass $minio_backend;
proxy_request_buffering off;
proxy_buffering off;
}
location / {
proxy_pass $web_backend;
}
}
+1 -1
View File
@@ -80,7 +80,7 @@ func Run(ctx context.Context, serviceName string) error {
if err != nil { if err != nil {
return fmt.Errorf("create memory embedding provider: %w", err) return fmt.Errorf("create memory embedding provider: %w", err)
} }
exportStore, err = objectstore.New(cfg.ObjectStoreEndpoint, cfg.ObjectStorePublic, cfg.ObjectStoreAccessKey, cfg.ObjectStoreSecretKey, cfg.ObjectStoreBucket, cfg.ObjectStoreRegion, cfg.ObjectStoreSecure) exportStore, err = objectstore.New(cfg.ObjectStoreEndpoint, cfg.ObjectStorePublic, cfg.ObjectStoreAccessKey, cfg.ObjectStoreSecretKey, cfg.ObjectStoreBucket, cfg.ObjectStoreRegion, cfg.ObjectStoreSecure, cfg.ObjectStorePublicSecure)
if err != nil { if err != nil {
return fmt.Errorf("create private export object store: %w", err) return fmt.Errorf("create private export object store: %w", err)
} }
+16 -2
View File
@@ -86,6 +86,7 @@ type Config struct {
ObjectStoreBucket string ObjectStoreBucket string
ObjectStoreRegion string ObjectStoreRegion string
ObjectStoreSecure bool ObjectStoreSecure bool
ObjectStorePublicSecure bool
ExportDownloadTTL time.Duration ExportDownloadTTL time.Duration
ExportObjectTTL time.Duration ExportObjectTTL time.Duration
MediaLocalDeleteGrace time.Duration MediaLocalDeleteGrace time.Duration
@@ -195,6 +196,10 @@ func Load(serviceName string) (Config, error) {
if err != nil { if err != nil {
return Config{}, err return Config{}, err
} }
objectStorePublicSecure, err := boolEnv("OBJECT_STORE_PUBLIC_SECURE", objectStoreSecure)
if err != nil {
return Config{}, err
}
environment := stringEnv("APP_ENV", EnvironmentLocal) environment := stringEnv("APP_ENV", EnvironmentLocal)
secretValues, err := loadSecretValues( secretValues, err := loadSecretValues(
"DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET", "DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET",
@@ -264,7 +269,7 @@ func Load(serviceName string) (Config, error) {
AuthIssuerURL: stringEnv("AUTH_ISSUER_URL", "http://localhost:58180/realms/voice-local"), AuthIssuerURL: stringEnv("AUTH_ISSUER_URL", "http://localhost:58180/realms/voice-local"),
AuthJWKSURL: stringEnv("AUTH_JWKS_URL", "http://localhost:58180/realms/voice-local/protocol/openid-connect/certs"), AuthJWKSURL: stringEnv("AUTH_JWKS_URL", "http://localhost:58180/realms/voice-local/protocol/openid-connect/certs"),
AuthAudience: stringEnv("AUTH_AUDIENCE", "voice-platform-web"), AuthAudience: stringEnv("AUTH_AUDIENCE", "voice-platform-web"),
OTLPEndpoint: stringEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:54317"), OTLPEndpoint: stringEnvAllowEmpty("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:54317"),
DependencyTimeout: dependencyTimeout, DependencyTimeout: dependencyTimeout,
ExternalProbeInterval: externalProbeInterval, ExternalProbeInterval: externalProbeInterval,
ShutdownTimeout: shutdownTimeout, ShutdownTimeout: shutdownTimeout,
@@ -304,6 +309,7 @@ func Load(serviceName string) (Config, error) {
ObjectStoreBucket: objectStoreBucket, ObjectStoreBucket: objectStoreBucket,
ObjectStoreRegion: objectStoreRegion, ObjectStoreRegion: objectStoreRegion,
ObjectStoreSecure: objectStoreSecure, ObjectStoreSecure: objectStoreSecure,
ObjectStorePublicSecure: objectStorePublicSecure,
ExportDownloadTTL: exportDownloadTTL, ExportDownloadTTL: exportDownloadTTL,
ExportObjectTTL: exportObjectTTL, ExportObjectTTL: exportObjectTTL,
MediaLocalDeleteGrace: mediaLocalDeleteGrace, MediaLocalDeleteGrace: mediaLocalDeleteGrace,
@@ -590,7 +596,7 @@ func (c Config) validateProduction() error {
} }
} }
if c.ServiceName == "api" || c.ServiceName == "worker" { if c.ServiceName == "api" || c.ServiceName == "worker" {
if !c.ObjectStoreSecure { if !c.ObjectStoreSecure || !c.ObjectStorePublicSecure {
return errors.New("production object storage must use TLS") return errors.New("production object storage must use TLS")
} }
if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) { if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) {
@@ -697,6 +703,14 @@ func stringEnv(key, fallback string) string {
return value return value
} }
func stringEnvAllowEmpty(key, fallback string) string {
value, exists := os.LookupEnv(key)
if !exists {
return fallback
}
return strings.TrimSpace(value)
}
func intEnv(key string, fallback int) (int, error) { func intEnv(key string, fallback int) (int, error) {
value := strings.TrimSpace(os.Getenv(key)) value := strings.TrimSpace(os.Getenv(key))
if value == "" { if value == "" {
+14
View File
@@ -27,6 +27,17 @@ func TestLoadUsesServicePortDefaults(t *testing.T) {
} }
} }
func TestLoadAllowsTracingExporterToBeDisabled(t *testing.T) {
t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "")
cfg, err := Load("api")
if err != nil {
t.Fatal(err)
}
if cfg.OTLPEndpoint != "" {
t.Fatalf("OTLPEndpoint = %q, want disabled exporter", cfg.OTLPEndpoint)
}
}
func TestLoadRejectsInvalidDuration(t *testing.T) { func TestLoadRejectsInvalidDuration(t *testing.T) {
t.Setenv("DEPENDENCY_TIMEOUT", "soon") t.Setenv("DEPENDENCY_TIMEOUT", "soon")
if _, err := Load("api"); err == nil { if _, err := Load("api"); err == nil {
@@ -118,6 +129,7 @@ func TestProductionAPIAcceptsHardenedConfiguration(t *testing.T) {
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40) cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40) cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
cfg.ObjectStoreSecure = true cfg.ObjectStoreSecure = true
cfg.ObjectStorePublicSecure = true
cfg.EmbeddingProvider = "ark" cfg.EmbeddingProvider = "ark"
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3" cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
cfg.ArkAPIKey = strings.Repeat("k", 40) cfg.ArkAPIKey = strings.Repeat("k", 40)
@@ -150,6 +162,7 @@ func TestProductionRejectsReusedSigningSecrets(t *testing.T) {
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40) cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40) cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
cfg.ObjectStoreSecure = true cfg.ObjectStoreSecure = true
cfg.ObjectStorePublicSecure = true
cfg.EmbeddingProvider = "ark" cfg.EmbeddingProvider = "ark"
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3" cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
cfg.ArkAPIKey = strings.Repeat("k", 40) cfg.ArkAPIKey = strings.Repeat("k", 40)
@@ -182,6 +195,7 @@ func TestProductionWorkerRequiresRealArkContentGenerator(t *testing.T) {
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40) cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40) cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
cfg.ObjectStoreSecure = true cfg.ObjectStoreSecure = true
cfg.ObjectStorePublicSecure = true
if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content generation") { if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content generation") {
t.Fatalf("Validate() error=%v, want local content generator production gate", err) t.Fatalf("Validate() error=%v, want local content generator production gate", err)
} }
+5 -4
View File
@@ -26,16 +26,17 @@ type Store struct {
bucket string bucket string
} }
func New(endpoint, publicEndpoint, accessKey, secretKey, bucket, region string, secure bool) (*Store, error) { func New(endpoint, publicEndpoint, accessKey, secretKey, bucket, region string, secure, publicSecure bool) (*Store, error) {
if endpoint == "" || publicEndpoint == "" || accessKey == "" || secretKey == "" || bucket == "" || region == "" { if endpoint == "" || publicEndpoint == "" || accessKey == "" || secretKey == "" || bucket == "" || region == "" {
return nil, errors.New("object store endpoints, credentials, bucket and region are required") return nil, errors.New("object store endpoints, credentials, bucket and region are required")
} }
options := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: secure, Region: region} internalOptions := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: secure, Region: region}
internalClient, err := minio.New(endpoint, options) internalClient, err := minio.New(endpoint, internalOptions)
if err != nil { if err != nil {
return nil, fmt.Errorf("create internal object store client: %w", err) return nil, fmt.Errorf("create internal object store client: %w", err)
} }
publicClient, err := minio.New(publicEndpoint, options) publicOptions := &minio.Options{Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: publicSecure, Region: region}
publicClient, err := minio.New(publicEndpoint, publicOptions)
if err != nil { if err != nil {
return nil, fmt.Errorf("create public object store signer: %w", err) return nil, fmt.Errorf("create public object store signer: %w", err)
} }
+4 -4
View File
@@ -15,10 +15,10 @@ import (
) )
func TestStoreValidationAndOfflineSigning(t *testing.T) { func TestStoreValidationAndOfflineSigning(t *testing.T) {
if _, err := New("", "localhost:59000", "key", "secret", "bucket", "us-east-1", false); err == nil { if _, err := New("", "localhost:59000", "key", "secret", "bucket", "us-east-1", false, false); err == nil {
t.Fatal("missing internal endpoint was accepted") t.Fatal("missing internal endpoint was accepted")
} }
store, err := New("localhost:59000", "localhost:59000", "local_minio", "local_minio_password", "private-exports", "us-east-1", false) store, err := New("localhost:59000", "localhost:59000", "local_minio", "local_minio_password", "private-exports", "us-east-1", false, true)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -31,7 +31,7 @@ func TestStoreValidationAndOfflineSigning(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
parsed, err := url.Parse(download) parsed, err := url.Parse(download)
if err != nil || parsed.Query().Get("X-Amz-Signature") == "" || parsed.Query().Get("response-content-disposition") != `attachment; filename="export.json"` { if err != nil || parsed.Scheme != "https" || parsed.Query().Get("X-Amz-Signature") == "" || parsed.Query().Get("response-content-disposition") != `attachment; filename="export.json"` {
t.Fatalf("download URL=%q parsed=%+v err=%v", download, parsed, err) t.Fatalf("download URL=%q parsed=%+v err=%v", download, parsed, err)
} }
if _, err := store.PresignUpload(ctx, "synthetic/firmware.bin", 0); err == nil { if _, err := store.PresignUpload(ctx, "synthetic/firmware.bin", 0); err == nil {
@@ -65,7 +65,7 @@ func TestStoreMinIOObjectLifecycle(t *testing.T) {
if endpoint == "" { if endpoint == "" {
t.Skip("TEST_OBJECT_STORE_ENDPOINT is not set") t.Skip("TEST_OBJECT_STORE_ENDPOINT is not set")
} }
store, err := New(endpoint, endpoint, "local_minio", "local_minio_password", "private-exports", "us-east-1", false) store, err := New(endpoint, endpoint, "local_minio", "local_minio_password", "private-exports", "us-east-1", false, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }