Compare commits
No commits in common. "a440600807832959e3a3e8445092ad95c90d8bc3" and "cbd1b9db28946d73fe2c68754b7a0e3acf7caeb6" have entirely different histories.
a440600807
...
cbd1b9db28
@ -1,34 +0,0 @@
|
|||||||
# Build context 瘦身 — 套用於「以專案根目錄為 build context」的 docker build
|
|
||||||
# (Dockerfile.real / Dockerfile.stub)。
|
|
||||||
#
|
|
||||||
# Dockerfile.real 用精準 COPY,不會把以下內容放進 image;此檔只是避免把它們
|
|
||||||
# 上傳給 daemon(省 ~4.3GB context 傳輸)。
|
|
||||||
|
|
||||||
# 大型版本控制目錄
|
|
||||||
.git
|
|
||||||
.gitmodules
|
|
||||||
|
|
||||||
# 被 USE_PREBUILD 取代的舊系統路徑 binaries(image 不需要,省 3.2GB)
|
|
||||||
libs/dynasty
|
|
||||||
libs/compiler
|
|
||||||
|
|
||||||
# c_sim_* 模擬器(NEF 轉檔不需要)
|
|
||||||
libs/c_sim_520
|
|
||||||
libs/c_sim_530
|
|
||||||
libs/c_sim_630
|
|
||||||
libs/c_sim_720
|
|
||||||
|
|
||||||
# Python / OS caches
|
|
||||||
**/__pycache__
|
|
||||||
**/*.pyc
|
|
||||||
**/*.pyo
|
|
||||||
.pytest_cache
|
|
||||||
.tmp
|
|
||||||
.mypy_cache
|
|
||||||
|
|
||||||
# 個人層 / 文件
|
|
||||||
.autoflow
|
|
||||||
docs
|
|
||||||
|
|
||||||
# macOS metadata
|
|
||||||
**/.DS_Store
|
|
||||||
@ -183,7 +183,7 @@ describe('getServiceToken — happy path & cache', () => {
|
|||||||
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses client_secret_post (form body, no Authorization header) for client credentials', async () => {
|
it('uses HTTP Basic auth header (not body) for client credentials', async () => {
|
||||||
const fetch = makeMockFetch(() => makeJsonResponse(200, tokenSuccessBody()));
|
const fetch = makeMockFetch(() => makeJsonResponse(200, tokenSuccessBody()));
|
||||||
const client = new OAuthClient({
|
const client = new OAuthClient({
|
||||||
fetch,
|
fetch,
|
||||||
@ -194,19 +194,20 @@ describe('getServiceToken — happy path & cache', () => {
|
|||||||
const init = fetch.calls[0].init;
|
const init = fetch.calls[0].init;
|
||||||
expect(init.method).toBe('POST');
|
expect(init.method).toBe('POST');
|
||||||
expect(init.headers['Content-Type']).toBe('application/x-www-form-urlencoded');
|
expect(init.headers['Content-Type']).toBe('application/x-www-form-urlencoded');
|
||||||
|
expect(init.headers.Authorization).toMatch(/^Basic /);
|
||||||
|
|
||||||
// 不可送 Basic auth header — MC 只接受 client_secret_post,送 Basic 會被拒。
|
const expected = Buffer.from(
|
||||||
// 鎖住這次的修正,避免被改回 Basic。
|
`${TEST_CLIENT_ID}:${TEST_CLIENT_SECRET}`,
|
||||||
expect(init.headers.Authorization).toBeUndefined();
|
'utf8'
|
||||||
|
).toString('base64');
|
||||||
|
expect(init.headers.Authorization).toBe(`Basic ${expected}`);
|
||||||
|
|
||||||
// client_id / client_secret 必須在 form body
|
// body 必須不含 client_secret
|
||||||
expect(typeof init.body).toBe('string');
|
expect(typeof init.body).toBe('string');
|
||||||
const params = new URLSearchParams(init.body);
|
expect(init.body).not.toContain(TEST_CLIENT_SECRET);
|
||||||
expect(params.get('grant_type')).toBe('client_credentials');
|
expect(init.body).toContain('grant_type=client_credentials');
|
||||||
expect(params.get('client_id')).toBe(TEST_CLIENT_ID);
|
expect(init.body).toContain('scope=files%3Aupload.write');
|
||||||
expect(params.get('client_secret')).toBe(TEST_CLIENT_SECRET);
|
expect(init.body).toContain(`audience=${TEST_FAA_AUDIENCE}`);
|
||||||
expect(params.get('scope')).toBe('files:upload.write');
|
|
||||||
expect(params.get('audience')).toBe(TEST_FAA_AUDIENCE);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('refreshes when cached token is within refreshSkewMs of expiry', async () => {
|
it('refreshes when cached token is within refreshSkewMs of expiry', async () => {
|
||||||
@ -779,6 +780,12 @@ describe('SECURITY: client_secret never appears in any log', () => {
|
|||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
describe('_internals helpers', () => {
|
describe('_internals helpers', () => {
|
||||||
|
it('buildBasicAuthHeader produces RFC 7617 base64 form', () => {
|
||||||
|
const h = _internals.buildBasicAuthHeader('alice', 'open sesame');
|
||||||
|
// base64 of "alice:open sesame" = "YWxpY2U6b3BlbiBzZXNhbWU="
|
||||||
|
expect(h).toBe('Basic YWxpY2U6b3BlbiBzZXNhbWU=');
|
||||||
|
});
|
||||||
|
|
||||||
it('parseTokenResponse handles minimal valid payload', () => {
|
it('parseTokenResponse handles minimal valid payload', () => {
|
||||||
const p = _internals.parseTokenResponse({
|
const p = _internals.parseTokenResponse({
|
||||||
access_token: 'a',
|
access_token: 'a',
|
||||||
@ -874,15 +881,19 @@ describe('integration with real HTTP server', () => {
|
|||||||
expect(tok).toBe('integration-token');
|
expect(tok).toBe('integration-token');
|
||||||
expect(captured).not.toBeNull();
|
expect(captured).not.toBeNull();
|
||||||
expect(captured.headers['content-type']).toBe('application/x-www-form-urlencoded');
|
expect(captured.headers['content-type']).toBe('application/x-www-form-urlencoded');
|
||||||
|
expect(captured.headers.authorization).toMatch(/^Basic /);
|
||||||
|
|
||||||
// client_secret_post:不送 Authorization header
|
const expectedBasic = Buffer.from(
|
||||||
expect(captured.headers.authorization).toBeUndefined();
|
`${TEST_CLIENT_ID}:${TEST_CLIENT_SECRET}`,
|
||||||
|
'utf8'
|
||||||
|
).toString('base64');
|
||||||
|
expect(captured.headers.authorization).toBe(`Basic ${expectedBasic}`);
|
||||||
|
|
||||||
|
// body 內不能含 client_secret
|
||||||
|
expect(captured.body).not.toContain(TEST_CLIENT_SECRET);
|
||||||
|
|
||||||
// client_id / client_secret 在 form body
|
|
||||||
const params = new URLSearchParams(captured.body);
|
const params = new URLSearchParams(captured.body);
|
||||||
expect(params.get('grant_type')).toBe('client_credentials');
|
expect(params.get('grant_type')).toBe('client_credentials');
|
||||||
expect(params.get('client_id')).toBe(TEST_CLIENT_ID);
|
|
||||||
expect(params.get('client_secret')).toBe(TEST_CLIENT_SECRET);
|
|
||||||
expect(params.get('scope')).toBe('files:upload.write');
|
expect(params.get('scope')).toBe('files:upload.write');
|
||||||
expect(params.get('audience')).toBe(TEST_FAA_AUDIENCE);
|
expect(params.get('audience')).toBe(TEST_FAA_AUDIENCE);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,19 +17,15 @@
|
|||||||
* 7. **絕不**將 client_secret / token 內容寫入 log
|
* 7. **絕不**將 client_secret / token 內容寫入 log
|
||||||
*
|
*
|
||||||
* 通信規格(對齊 TDD §2.4 / §5.2 / RFC 6749 §4.4 + §2.3.1):
|
* 通信規格(對齊 TDD §2.4 / §5.2 / RFC 6749 §4.4 + §2.3.1):
|
||||||
* - client 認證採 **`client_secret_post`**:`client_id` / `client_secret` 放在
|
* - 使用 HTTP Basic auth header `Authorization: Basic base64(client_id:client_secret)`
|
||||||
* POST form body(不是 HTTP Basic auth header)。Member Center(OpenIddict)
|
* (RFC 6749 §2.3.1 推薦,比 body 傳 secret 安全;token endpoint 通常都接受)
|
||||||
* 只接受這種送法,回 `Basic` header 會被拒為 `401 invalid_client`。
|
|
||||||
* (RFC 6749 §2.3.1 把 `client_secret_post` 列為允許的 client 認證方式之一。)
|
|
||||||
* - body: `application/x-www-form-urlencoded`,含 `grant_type=client_credentials`、
|
* - body: `application/x-www-form-urlencoded`,含 `grant_type=client_credentials`、
|
||||||
* `client_id`、`client_secret`、`scope=<scope>`、`audience=<aud>`。
|
* `scope=<scope>`、`audience=<aud>`(Auth0 / 多數 IdP 慣例)
|
||||||
* - 預期回應 JSON:`{ access_token, token_type, expires_in }`
|
* - 預期回應 JSON:`{ access_token, token_type, expires_in }`
|
||||||
*
|
*
|
||||||
* 安全注意:
|
* 安全注意:
|
||||||
* - `client_secret` 雖然放進 body,但**絕不**得出現在任何 log
|
* - 任何 log 都不得包含 `client_secret`、Authorization header 內容、access_token
|
||||||
* (URLSearchParams 字串、body 變數、錯誤訊息都不可被 log 出來)。
|
* - 錯誤訊息只揭露 status + 標準 error_code(如 `invalid_client`),不揭露 server 端細節
|
||||||
* - 任何 log 都不得包含 `client_secret`、access_token。
|
|
||||||
* - 錯誤訊息只揭露 status + 標準 error_code(如 `invalid_client`),不揭露 server 端細節。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@ -85,6 +81,19 @@ class OAuthTimeoutError extends OAuthError {
|
|||||||
// 內部 helpers
|
// 內部 helpers
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 client_id / client_secret 編碼成 Basic auth header value。
|
||||||
|
*
|
||||||
|
* @param {string} clientId
|
||||||
|
* @param {string} clientSecret
|
||||||
|
* @returns {string} - `Basic <base64>`
|
||||||
|
*/
|
||||||
|
function buildBasicAuthHeader(clientId, clientSecret) {
|
||||||
|
const raw = `${clientId}:${clientSecret}`;
|
||||||
|
// Buffer.from(...).toString('base64') 是 Node 標準做法;不依賴 deprecated `btoa`
|
||||||
|
return `Basic ${Buffer.from(raw, 'utf8').toString('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 從 fetch Response 嘗試解析 OAuth 標準錯誤 JSON:
|
* 從 fetch Response 嘗試解析 OAuth 標準錯誤 JSON:
|
||||||
* `{ "error": "invalid_client", "error_description": "..." }`
|
* `{ "error": "invalid_client", "error_description": "..." }`
|
||||||
@ -287,12 +296,8 @@ class OAuthClient {
|
|||||||
* @returns {Promise<string>}
|
* @returns {Promise<string>}
|
||||||
*/
|
*/
|
||||||
async _fetchToken(scope, config) {
|
async _fetchToken(scope, config) {
|
||||||
// client 認證採 client_secret_post:client_id / client_secret 放在 form body。
|
|
||||||
// 注意:body 含 client_secret,**絕不**可被 log 出來(見檔頭安全注意)。
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
client_id: config.clientId,
|
|
||||||
client_secret: config.clientSecret,
|
|
||||||
scope,
|
scope,
|
||||||
audience: config.faaAudience,
|
audience: config.faaAudience,
|
||||||
}).toString();
|
}).toString();
|
||||||
@ -300,7 +305,7 @@ class OAuthClient {
|
|||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
Accept: 'application/json',
|
Accept: 'application/json',
|
||||||
// 不送 Authorization Basic header;MC 只接受 client_secret_post。
|
Authorization: buildBasicAuthHeader(config.clientId, config.clientSecret),
|
||||||
};
|
};
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
@ -451,6 +456,7 @@ module.exports = {
|
|||||||
|
|
||||||
// 測試用內部
|
// 測試用內部
|
||||||
_internals: {
|
_internals: {
|
||||||
|
buildBasicAuthHeader,
|
||||||
parseTokenResponse,
|
parseTokenResponse,
|
||||||
tryParseOauthErrorBody,
|
tryParseOauthErrorBody,
|
||||||
singleton,
|
singleton,
|
||||||
|
|||||||
@ -720,14 +720,11 @@ describe('POST /api/v1/jobs — 201 happy path', () => {
|
|||||||
// MinIO:model + 2 個 ref_images
|
// MinIO:model + 2 個 ref_images
|
||||||
expect(ctx.minio.uploadToMinIO).toHaveBeenCalledTimes(3);
|
expect(ctx.minio.uploadToMinIO).toHaveBeenCalledTimes(3);
|
||||||
const keys = ctx.minio._uploaded.map((u) => u.key);
|
const keys = ctx.minio._uploaded.map((u) => u.key);
|
||||||
// ref_images 必須在 `input/ref_images/` 下,與 bie worker 讀取端
|
|
||||||
// (consumer.py `jobs/{id}/input/ref_images`) + local backend + legacy.js
|
|
||||||
// 約定一致。2026-06-22 修復:原本少 `input/` 一層導致 bie e2e blocker。
|
|
||||||
expect(keys).toEqual(
|
expect(keys).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.stringMatching(/^jobs\/[^/]+\/input\/model\.onnx$/),
|
expect.stringMatching(/^jobs\/[^/]+\/input\/model\.onnx$/),
|
||||||
expect.stringMatching(/^jobs\/[^/]+\/input\/ref_images\/0_a\.jpg$/),
|
expect.stringMatching(/^jobs\/[^/]+\/ref_images\/0_a\.jpg$/),
|
||||||
expect.stringMatching(/^jobs\/[^/]+\/input\/ref_images\/1_b\.png$/),
|
expect.stringMatching(/^jobs\/[^/]+\/ref_images\/1_b\.png$/),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -105,13 +105,9 @@ describe('jobService.writeInputToMinIO', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(result.inputObjectKey).toBe('jobs/job-123/input/model.onnx');
|
expect(result.inputObjectKey).toBe('jobs/job-123/input/model.onnx');
|
||||||
// ref_images 必須在 `input/ref_images/` 下,與 bie worker
|
|
||||||
// (consumer.py download `jobs/{id}/input/ref_images`) + local backend
|
|
||||||
// (storage/local.js `<id>/input/ref_images/`) + legacy.js 三方約定一致。
|
|
||||||
// 2026-06-22 修復:原本少了 `input/` 一層導致 bie worker 抓空 prefix → e2e fail。
|
|
||||||
expect(result.refImageObjectKeys).toEqual([
|
expect(result.refImageObjectKeys).toEqual([
|
||||||
'jobs/job-123/input/ref_images/0_a.jpg',
|
'jobs/job-123/ref_images/0_a.jpg',
|
||||||
'jobs/job-123/input/ref_images/1_b.png',
|
'jobs/job-123/ref_images/1_b.png',
|
||||||
]);
|
]);
|
||||||
expect(result.uploadedKeys).toHaveLength(3);
|
expect(result.uploadedKeys).toHaveLength(3);
|
||||||
expect(minio.uploadToMinIO).toHaveBeenCalledTimes(3);
|
expect(minio.uploadToMinIO).toHaveBeenCalledTimes(3);
|
||||||
@ -413,27 +409,10 @@ describe('jobService._internals (object key naming)', () => {
|
|||||||
minio: makeFakeMinio(),
|
minio: makeFakeMinio(),
|
||||||
});
|
});
|
||||||
expect(svc._internals.buildRefImageObjectKey('j-1', 0, 'a.jpg')).toBe(
|
expect(svc._internals.buildRefImageObjectKey('j-1', 0, 'a.jpg')).toBe(
|
||||||
'jobs/j-1/input/ref_images/0_a.jpg'
|
'jobs/j-1/ref_images/0_a.jpg'
|
||||||
);
|
);
|
||||||
expect(svc._internals.buildRefImageObjectKey('j-1', 1, 'a.jpg')).toBe(
|
expect(svc._internals.buildRefImageObjectKey('j-1', 1, 'a.jpg')).toBe(
|
||||||
'jobs/j-1/input/ref_images/1_a.jpg'
|
'jobs/j-1/ref_images/1_a.jpg'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Read/write contract guard:寫入端 (jobService) 產出的 ref_images key prefix
|
|
||||||
// 必須等於 bie worker (consumer.py) download_prefix 的 `jobs/{id}/input/ref_images`。
|
|
||||||
// 這條鎖死 scheduler↔worker 的 S3 key 約定,避免兩端路徑再次漂移
|
|
||||||
// (2026-06-22 real-mode e2e blocker 的根因)。
|
|
||||||
it('buildRefImageObjectKey key prefix matches bie worker download_prefix contract', () => {
|
|
||||||
const svc = createJobService({
|
|
||||||
redis: makeFakeRedis(),
|
|
||||||
sseService: makeFakeSseService(),
|
|
||||||
minio: makeFakeMinio(),
|
|
||||||
});
|
|
||||||
const jobId = 'contract-job';
|
|
||||||
// consumer.py:93-94 — bie worker download_prefix(f"{jobs/{job_id}}/input/ref_images")
|
|
||||||
const workerDownloadPrefix = `jobs/${jobId}/input/ref_images`;
|
|
||||||
const writeKey = svc._internals.buildRefImageObjectKey(jobId, 0, 'cal.jpg');
|
|
||||||
expect(writeKey.startsWith(`${workerDownloadPrefix}/`)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -335,25 +335,12 @@ function createJobService(deps) {
|
|||||||
* Ref image object key(對齊 TDD §6.1)。
|
* Ref image object key(對齊 TDD §6.1)。
|
||||||
* 加入 index 前綴避免同名衝突。
|
* 加入 index 前綴避免同名衝突。
|
||||||
*
|
*
|
||||||
* 路徑約定:`jobs/{jobId}/input/ref_images/...`。
|
|
||||||
* ref_images 屬於 job 的「input」資產,與 model 檔(buildInputObjectKey 的
|
|
||||||
* `jobs/{jobId}/input/{filename}`)同處 `input/` 命名空間下,並與以下三處
|
|
||||||
* 既有約定一致:
|
|
||||||
* - local backend:storage/local.js 寫到 `<jobId>/input/ref_images/`
|
|
||||||
* - legacy MinIO path:routes/legacy.js 寫到 `jobs/{jobId}/input/ref_images/`
|
|
||||||
* - bie worker 讀取端:services/workers/consumer.py 從
|
|
||||||
* `jobs/{jobId}/input/ref_images/` download(且 _build_input_paths 對
|
|
||||||
* local/minio 兩種 backend 共用 `input/ref_images` 落地路徑)
|
|
||||||
* 2026-06-22 修復:原本此處少了 `input/` 一層(v1 path 的孤例),導致
|
|
||||||
* bie worker 抓空 prefix → dataset 目錄不存在 → bie job fail(real-mode
|
|
||||||
* e2e blocker)。對齊到 `input/ref_images` 後與上述三處一致。
|
|
||||||
*
|
|
||||||
* @param {string} jobId
|
* @param {string} jobId
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
* @param {string} safeFilename
|
* @param {string} safeFilename
|
||||||
*/
|
*/
|
||||||
function buildRefImageObjectKey(jobId, index, safeFilename) {
|
function buildRefImageObjectKey(jobId, index, safeFilename) {
|
||||||
return `jobs/${jobId}/input/ref_images/${index}_${safeFilename}`;
|
return `jobs/${jobId}/ref_images/${index}_${safeFilename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
##
|
|
||||||
# Kneron Model Converter — REAL worker override(真實轉檔版)
|
|
||||||
#
|
|
||||||
# 用途:把 onnx/bie/nef 三個 worker 從 stub 切成真實轉檔 image
|
|
||||||
# (jim800121/kneron-model-converter-worker:real-20260622)
|
|
||||||
#
|
|
||||||
# 部署(staging):
|
|
||||||
# export DOCKER_HOST=tcp://192.168.0.130:2375
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.real.yml pull onnx-worker bie-worker nef-worker
|
|
||||||
# docker compose -f docker-compose.yml -f docker-compose.real.yml up -d onnx-worker bie-worker nef-worker
|
|
||||||
#
|
|
||||||
# Rollback 回 stub(秒退、不靠 Docker Hub):
|
|
||||||
# export DOCKER_HOST=tcp://192.168.0.130:2375
|
|
||||||
# # 直接用 base compose(不帶 -f docker-compose.real.yml)重啟,會用回本地 stub image:
|
|
||||||
# docker compose -f docker-compose.yml up -d --build onnx-worker bie-worker nef-worker
|
|
||||||
# # (stub image 還在 staging 本地;--build 確保用回 Dockerfile.stub。
|
|
||||||
# # 或更快:base compose 的 stub image 還在,省略 --build 直接 up -d 即用回原 stub container)
|
|
||||||
#
|
|
||||||
# 設計:override 只覆寫「image / WORKER_MODE」,其餘(STAGE / REDIS / MinIO env / volumes /
|
|
||||||
# depends_on / restart)沿用 base compose。base 的 build: 區段保留不刪 → rollback 乾淨。
|
|
||||||
# STORAGE_BACKEND=minio 由 .env 提供(已驗證),三個 worker 沿用 base 的
|
|
||||||
# STORAGE_BACKEND=${STORAGE_BACKEND:-local} → 最終生效 minio。
|
|
||||||
##
|
|
||||||
|
|
||||||
services:
|
|
||||||
onnx-worker:
|
|
||||||
image: jim800121/kneron-model-converter-worker:real-20260622
|
|
||||||
pull_policy: always
|
|
||||||
environment:
|
|
||||||
- WORKER_MODE=real
|
|
||||||
|
|
||||||
bie-worker:
|
|
||||||
image: jim800121/kneron-model-converter-worker:real-20260622
|
|
||||||
pull_policy: always
|
|
||||||
environment:
|
|
||||||
- WORKER_MODE=real
|
|
||||||
|
|
||||||
nef-worker:
|
|
||||||
image: jim800121/kneron-model-converter-worker:real-20260622
|
|
||||||
pull_policy: always
|
|
||||||
environment:
|
|
||||||
- WORKER_MODE=real
|
|
||||||
@ -211,7 +211,7 @@ visionA-backend Member Center Converter
|
|||||||
```
|
```
|
||||||
inputObjectKey = `jobs/${jobId}/input/${safeFilename}`
|
inputObjectKey = `jobs/${jobId}/input/${safeFilename}`
|
||||||
^uuidv4 ^server-controlled prefix ^sanitized
|
^uuidv4 ^server-controlled prefix ^sanitized
|
||||||
refImageKey = `jobs/${jobId}/input/ref_images/${index}_${safeFilename}`
|
refImageKey = `jobs/${jobId}/ref_images/${index}_${safeFilename}`
|
||||||
```
|
```
|
||||||
|
|
||||||
attacker 無法控制:
|
attacker 無法控制:
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
# Real Worker Dockerfile — 自帶 prebuild toolchain,可實際把 BIE→NEF 真實轉檔
|
|
||||||
#
|
|
||||||
# 與 Dockerfile.stub 的差異:本 image 內含 ktc/ 真實轉檔素材 + toolchain/prebuild
|
|
||||||
# 的 x86-64 編譯 binaries(batch_compile / kneron_nef_utils / model_converter),
|
|
||||||
# 並設好 USE_PREBUILD/LD_LIBRARY_PATH/PYTHONPATH,讓 import ktc + ktc.compile 走
|
|
||||||
# repo 自帶 prebuild 路徑(不抓系統 libs/compiler、libs/dynasty)。
|
|
||||||
#
|
|
||||||
# Build(從專案根目錄、本機 Docker daemon):
|
|
||||||
# docker build -f services/workers/Dockerfile.real -t kneron-worker-real:verify .
|
|
||||||
#
|
|
||||||
# 注意:build context 含 toolchain/prebuild (~2GB)。搭配「專案根 .dockerignore」
|
|
||||||
# (docker build 以專案根為 context,只讀 context 根目錄的 .dockerignore;不存在
|
|
||||||
# services/workers/.dockerignore)排除 libs/dynasty + libs/compiler(被 USE_PREBUILD
|
|
||||||
# 取代,省 ~3.2GB)。
|
|
||||||
|
|
||||||
# python:3.9 對齊既有 worker;bullseye(Debian 11, glibc 2.31) 對舊 toolchain
|
|
||||||
# (prebuild ELF 最低需求 GNU/Linux 3.2.0)相容性最佳。
|
|
||||||
FROM python:3.9-slim-bullseye
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# --- System libraries ---------------------------------------------------------
|
|
||||||
# opencv-python runtime: libgl1 / libglib2.0-0
|
|
||||||
# OpenMP / OpenBLAS(compiler binary 會用): libgomp1
|
|
||||||
# gfortran runtime(prebuild/lib 雖自帶 libgfortran.so.5,仍裝保險): libgfortran5
|
|
||||||
# 其餘缺的 .so 靠 image 內 `ldd toolchain/prebuild/batch_compile` 的 "not found" 補。
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
libgl1 \
|
|
||||||
libglib2.0-0 \
|
|
||||||
libgomp1 \
|
|
||||||
libgfortran5 \
|
|
||||||
ca-certificates \
|
|
||||||
procps \
|
|
||||||
file \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# --- Python dependencies ------------------------------------------------------
|
|
||||||
# 1) queue / storage(consumer.py 需要)
|
|
||||||
# 2) worker 既有 requirements(fastapi/onnx/opencv…)
|
|
||||||
# 3) ktc → sys_flow / sys_flow_v2 + extract_bie_info + kneronnxopt module-load 額外
|
|
||||||
# 需要(全部由本機實跑 `import ktc` 逐個盤出):
|
|
||||||
# pandas blinker docopt snoop pyzipper dict_recursive_update beautifulsoup4
|
|
||||||
# jinja2 matplotlib tabulate commentjson IPython adjustText onnx_tool onnxsim
|
|
||||||
#
|
|
||||||
# 特別注意的版本鎖:
|
|
||||||
# - onnx==1.14.1:與 root requirements 一致;ktc/toolchain.py 對 onnx==1.7.0 會
|
|
||||||
# 關閉 730 支援,必須避開 1.7.0。
|
|
||||||
# - onnx_tool==0.7.0:未鎖時裝到的新版在 import 期會踩
|
|
||||||
# "unsupported operand type(s) for |: MessageMeta and type"(protobuf 衝突);
|
|
||||||
# 0.7.0 import 乾淨。
|
|
||||||
# - onnxsim==0.4.36(--only-binary):py3.9/bullseye 上新版 onnxsim 無 wheel、需
|
|
||||||
# cmake 從源碼編;0.4.36 有 prebuilt wheel,避免裝 build toolchain。
|
|
||||||
# 4) numpy<2 最後裝:prebuild 鏈的 C-extension 以 numpy 1.x 編譯,numpy 2.x 會出
|
|
||||||
# "numpy.core.multiarray failed to import"。最後安裝確保不被其他套件升級回 2.x。
|
|
||||||
COPY services/workers/nef/requirements.txt /tmp/worker-requirements.txt
|
|
||||||
RUN pip install --no-cache-dir \
|
|
||||||
redis>=5.0 boto3>=1.28 \
|
|
||||||
&& pip install --no-cache-dir -r /tmp/worker-requirements.txt \
|
|
||||||
&& pip install --no-cache-dir \
|
|
||||||
"onnx==1.14.1" \
|
|
||||||
scipy \
|
|
||||||
pandas blinker docopt snoop \
|
|
||||||
pyzipper dict_recursive_update beautifulsoup4 jinja2 matplotlib tabulate \
|
|
||||||
commentjson IPython adjustText "onnx_tool==0.7.0" \
|
|
||||||
&& pip install --no-cache-dir --only-binary :all: "onnxsim==0.4.36" \
|
|
||||||
&& pip install --no-cache-dir "numpy<2"
|
|
||||||
|
|
||||||
# --- Application + toolchain source ------------------------------------------
|
|
||||||
# 精準 COPY,避免把 libs/dynasty(2.0G) + libs/compiler(1.2G) 帶進來。
|
|
||||||
COPY ktc/ /app/ktc/
|
|
||||||
COPY vendor/ /app/vendor/
|
|
||||||
COPY services/ /app/services/
|
|
||||||
COPY toolchain/prebuild/ /app/toolchain/prebuild/
|
|
||||||
COPY libs/kneronnxopt/ /app/libs/kneronnxopt/
|
|
||||||
COPY libs/ONNX_Convertor/ /app/libs/ONNX_Convertor/
|
|
||||||
COPY libs/fpAnalyser/ /app/libs/fpAnalyser/
|
|
||||||
COPY E2E_Simulator/python_flow/ /app/E2E_Simulator/python_flow/
|
|
||||||
# 驗證用 fixtures + conftest(production 部署可拿掉,這裡為了在 image 內端到端跑)
|
|
||||||
COPY tests/ /app/tests/
|
|
||||||
|
|
||||||
RUN mkdir -p /data/jobs
|
|
||||||
|
|
||||||
# --- Toolchain / runtime environment(即 tests/conftest.py 的 production 化版本)---
|
|
||||||
ENV USE_PREBUILD=/app/toolchain/prebuild
|
|
||||||
ENV LD_LIBRARY_PATH=/app/toolchain/prebuild/lib
|
|
||||||
ENV PYTHONPATH=/app:/app/vendor:/app/libs:/app/libs/kneronnxopt:/app/E2E_Simulator/python_flow
|
|
||||||
ENV KTC_DISABLE_MP=1
|
|
||||||
|
|
||||||
# --- Worker runtime config ----------------------------------------------------
|
|
||||||
ENV WORKER_MODE=real
|
|
||||||
ENV REDIS_URL=redis://redis:6379
|
|
||||||
ENV JOB_DATA_DIR=/data/jobs
|
|
||||||
# STORAGE_BACKEND 刻意「不」在此 image 設預設值。
|
|
||||||
#
|
|
||||||
# 為什麼:consumer.py 第 52 行 `os.environ.get("STORAGE_BACKEND", "local")` 本身
|
|
||||||
# 已會 fall back 到 local。若這支真實版 image 又寫死 ENV STORAGE_BACKEND=local,會給
|
|
||||||
# 部署者「image 預設就能上雲」的錯覺;一旦 staging compose 漏設 STORAGE_BACKEND=minio,
|
|
||||||
# worker 會 *安靜* 走 local(產出寫 container 本機磁碟、不上 MinIO)→ 轉檔成功但
|
|
||||||
# visionA poll 拿不到結果(即 commit b8457dd / cbd1b9d 修過的同類隱患)。
|
|
||||||
#
|
|
||||||
# 處理姿態:
|
|
||||||
# - 本 image 不設預設 → 由 compose / 部署環境「顯式」提供。
|
|
||||||
# - staging(用 MinIO)必設 `STORAGE_BACKEND=minio`(見部署 checklist 硬性項)。
|
|
||||||
# - 本機 / dev 不設時,consumer.py 仍 fall back local,stub/local 開發流程不受影響。
|
|
||||||
# - 註:要根除 silent fallback 的最穩做法是在 consumer.py 對 WORKER_MODE=real 做
|
|
||||||
# fail-fast(漏設 minio 即報錯),但那屬 application code、需 backend 處理,
|
|
||||||
# 不在本 Dockerfile 範圍。
|
|
||||||
# STAGE: onnx / bie / nef(三個 worker 共用此 image,差在 entrypoint)
|
|
||||||
ENV STAGE=nef
|
|
||||||
|
|
||||||
# 用 shell form 以便 ${STAGE} 變數展開(三個 worker 共用 image、差在此變數)。
|
|
||||||
CMD ["sh", "-c", "python -m services.workers.${STAGE}.worker"]
|
|
||||||
Loading…
x
Reference in New Issue
Block a user