From 05cbdc2feb4862df93586b76f0ec6502add83e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=B5=81?= Date: Mon, 24 Aug 2026 21:37:24 +0800 Subject: [PATCH 1/5] feat: support microSandboxConfig.image for container runtime - add getContainerImage helper, preferring microSandboxConfig.image over customContainerConfig.image - resolve container image via getContainerImage in build/deploy/local and acceleration-wait logs - include microSandboxConfig when updating function config - add image to IMicroSandboxConfig interface and schema; relax schema requirement to either config --- __tests__/ut/resources/fc/impl/utils_test.ts | 23 +++++++++++++ publish.yaml | 2 +- src/base.ts | 11 +++++-- src/interface/function.ts | 1 + src/resources/fc/impl/utils.ts | 19 ++++++++++- src/resources/fc/index.ts | 34 +++++++++++--------- src/schema.json | 18 +++++++++-- src/subCommands/build/impl/baseBuilder.ts | 2 +- src/subCommands/deploy/impl/function.ts | 4 +-- src/subCommands/local/impl/baseLocal.ts | 2 +- 10 files changed, 89 insertions(+), 27 deletions(-) diff --git a/__tests__/ut/resources/fc/impl/utils_test.ts b/__tests__/ut/resources/fc/impl/utils_test.ts index 2a7dd3d7..7c572df0 100644 --- a/__tests__/ut/resources/fc/impl/utils_test.ts +++ b/__tests__/ut/resources/fc/impl/utils_test.ts @@ -4,6 +4,7 @@ import { getRemoteResourceConfig, computeLocalAuto, getCustomEndpoint, + getContainerImage, } from '../../../../../src/resources/fc/impl/utils'; import { INasConfig, IVpcConfig, ILogConfig, IOssMountConfig } from '../../../../../src/interface'; import * as utils from '../../../../../src/utils'; @@ -68,6 +69,28 @@ describe('utils', () => { }); }); + describe('getContainerImage', () => { + it('should prefer microSandboxConfig.image over customContainerConfig.image', () => { + const result = getContainerImage({ + microSandboxConfig: { image: 'registry/sandbox:v1' }, + customContainerConfig: { image: 'registry/container:v1' }, + }); + expect(result).toBe('registry/sandbox:v1'); + }); + + it('should fall back to customContainerConfig.image', () => { + const result = getContainerImage({ + customContainerConfig: { image: 'registry/container:v1' }, + }); + expect(result).toBe('registry/container:v1'); + }); + + it('should return undefined when no image is configured', () => { + expect(getContainerImage({})).toBeUndefined(); + expect(getContainerImage({ customContainerConfig: { image: '' } })).toBeUndefined(); + }); + }); + describe('getRemoteResourceConfig', () => { it('should extract remote resource configurations correctly', () => { const mockRemote = { diff --git a/publish.yaml b/publish.yaml index 8f8a580f..643e2a98 100644 --- a/publish.yaml +++ b/publish.yaml @@ -3,7 +3,7 @@ Type: Component Name: fc3 Provider: - 阿里云 -Version: 0.1.24 +Version: 0.1.25 Description: 阿里云函数计算全生命周期管理 HomePage: https://github.com/devsapp/fc3 Organization: 阿里云函数计算(FC) diff --git a/src/base.ts b/src/base.ts index aad46cf1..efac0338 100644 --- a/src/base.ts +++ b/src/base.ts @@ -39,9 +39,14 @@ export default class Base { _.set(inputs, 'props.endpoint', argvEndpoint); } // fc组件镜像 trim 左右空格 - const image = _.get(inputs, 'props.customContainerConfig.image'); - if (!_.isEmpty(image)) { - _.set(inputs, 'props.customContainerConfig.image', _.trim(image)); + for (const imagePath of [ + 'props.microSandboxConfig.image', + 'props.customContainerConfig.image', + ]) { + const image = _.get(inputs, imagePath); + if (!_.isEmpty(image)) { + _.set(inputs, imagePath, _.trim(image)); + } } const role = _.get(inputs, 'props.role'); diff --git a/src/interface/function.ts b/src/interface/function.ts index d3fe6082..a4af47b2 100644 --- a/src/interface/function.ts +++ b/src/interface/function.ts @@ -76,6 +76,7 @@ export interface ILogConfig { } export interface IMicroSandboxConfig { + image?: string; osType?: string; readyCommand?: string; startCommand?: string; diff --git a/src/resources/fc/impl/utils.ts b/src/resources/fc/impl/utils.ts index f4b46aab..0027ba76 100644 --- a/src/resources/fc/impl/utils.ts +++ b/src/resources/fc/impl/utils.ts @@ -1,5 +1,12 @@ import _ from 'lodash'; -import { INasConfig, IVpcConfig, ILogConfig, Runtime, IOssMountConfig } from '../../../interface'; +import { + INasConfig, + IVpcConfig, + ILogConfig, + Runtime, + IOssMountConfig, + IFunction, +} from '../../../interface'; import { isAuto, isAutoVpcConfig } from '../../../utils'; import logger from '../../../logger'; import * as fs from 'fs'; @@ -10,6 +17,16 @@ export function isCustomContainerRuntime(runtime: string): boolean { return runtime === Runtime['custom-container'] || runtime === Runtime['micro-sandbox']; } +/** + * microSandboxConfig.image 优先级高于 customContainerConfig.image + */ +export function getContainerImage( + props: Pick, +): string | undefined { + const image = props?.microSandboxConfig?.image || props?.customContainerConfig?.image; + return _.isEmpty(image) ? undefined : image; +} + export function isCustomRuntime(runtime: string): boolean { return ( runtime === Runtime.custom || diff --git a/src/resources/fc/index.ts b/src/resources/fc/index.ts index 0849ce50..25ff582a 100644 --- a/src/resources/fc/index.ts +++ b/src/resources/fc/index.ts @@ -61,7 +61,12 @@ import { isFunctionStateWaitTimedOut, isFunctionScalingConfigError, } from './error-code'; -import { isCustomContainerRuntime, isCustomRuntime, computeLocalAuto } from './impl/utils'; +import { + isCustomContainerRuntime, + isCustomRuntime, + computeLocalAuto, + getContainerImage, +} from './impl/utils'; import replaceFunctionConfig from './impl/replace-function-config'; import { IAlias } from '../../interface/cli-config/alias'; import { TriggerType } from '../../interface/base'; @@ -79,6 +84,7 @@ export default class FC extends FC_Client { static computeLocalAuto = computeLocalAuto; static isCustomContainerRuntime = isCustomContainerRuntime; static isCustomRuntime = isCustomRuntime; + static getContainerImage = getContainerImage; static replaceFunctionConfig = replaceFunctionConfig; async untilFunctionStateOK(config: IFunction, reason: string, skipAccelerationWait?: boolean) { @@ -93,9 +99,10 @@ export default class FC extends FC_Client { const retryContainerAccelerated = FC.isCustomContainerRuntime(config.runtime); // 部署镜像需要重试 3min, 直到达到!(State == Pending || LastUpdateStatus == InProgress) if (retryContainerAccelerated) { + const image = getContainerImage(config); if (skipAccelerationWait) { logger.info( - `Skip waiting for ${config.customContainerConfig.image} optimization. The function will be available for invocation once the image acceleration process is complete.`, + `Skip waiting for ${image} optimization. The function will be available for invocation once the image acceleration process is complete.`, ); return; } @@ -103,24 +110,24 @@ export default class FC extends FC_Client { if (reason === 'CREATE') { if (isAppCenter()) { logger.info( - `${config.customContainerConfig.image} optimization to be ready, the function will be available for invocation once this process is complete`, + `${image} optimization to be ready, the function will be available for invocation once this process is complete`, ); } else { logger.spin( 'checking', - `${config.customContainerConfig.image} `, + `${image} `, `optimization to be ready, the function will be available for invocation once this process is complete ...`, ); } } else if (reason === 'UPDATE') { if (isAppCenter()) { logger.info( - `${config.customContainerConfig.image} optimization to be ready, function calls will be updated to the latest deployed version once the image optimization process is complete ...`, + `${image} optimization to be ready, function calls will be updated to the latest deployed version once the image optimization process is complete ...`, ); } else { logger.spin( 'checking', - `${config.customContainerConfig.image}`, + `${image}`, `optimization to be ready, function calls will be updated to the latest deployed version once the image optimization process is complete ...`, ); } @@ -145,16 +152,14 @@ export default class FC extends FC_Client { await sleep(retryInterval); if (isAppCenter()) { logger.info( - `${ - config.customContainerConfig.image - } optimization is not ready, function state=${state}, lastUpdateStatus=${lastUpdateStatus}, waiting ${ + `${image} optimization is not ready, function state=${state}, lastUpdateStatus=${lastUpdateStatus}, waiting ${ (new Date().getTime() - startTime) / 1000 } seconds...`, ); } else { logger.spin( 'checking', - `${config.customContainerConfig.image}`, + `${image}`, `optimization is not ready, function state=${state}, lastUpdateStatus=${lastUpdateStatus}, waiting ${ (new Date().getTime() - startTime) / 1000 } seconds...`, @@ -172,13 +177,9 @@ export default class FC extends FC_Client { await sleep(retryInterval); } else { if (isAppCenter()) { - logger.info(`${config.customContainerConfig.image} optimization is ready`); + logger.info(`${image} optimization is ready`); } else { - logger.spin( - 'checked', - `${config.customContainerConfig.image}`, - `optimization is ready`, - ); + logger.spin('checked', `${image}`, `optimization is ready`); } break; } @@ -290,6 +291,7 @@ export default class FC extends FC_Client { functionName: config.functionName, code: config.code, customContainerConfig: config.customContainerConfig, + microSandboxConfig: config.microSandboxConfig, } as any; } else if (type === 'config') { _.unset(config, 'code'); diff --git a/src/schema.json b/src/schema.json index c887c399..7dc9b5d4 100644 --- a/src/schema.json +++ b/src/schema.json @@ -639,6 +639,9 @@ }, "IMicroSandboxConfig": { "properties": { + "image": { + "type": "string" + }, "osType": { "type": "string" }, @@ -1454,8 +1457,19 @@ "required": [ "region", "functionName", - "runtime", - "customContainerConfig" + "runtime" + ], + "anyOf": [ + { + "required": [ + "microSandboxConfig" + ] + }, + { + "required": [ + "customContainerConfig" + ] + } ] }, "else": { diff --git a/src/subCommands/build/impl/baseBuilder.ts b/src/subCommands/build/impl/baseBuilder.ts index 26a2af6f..138df8e9 100644 --- a/src/subCommands/build/impl/baseBuilder.ts +++ b/src/subCommands/build/impl/baseBuilder.ts @@ -80,7 +80,7 @@ export abstract class Builder { async getRuntimeBuildImage(): Promise { let image: string; if (FC.isCustomContainerRuntime(this.getRuntime())) { - image = this.getProps().customContainerConfig?.image; + image = FC.getContainerImage(this.getProps()); if (_.isEmpty(image)) { throw new Error('image must be set in custom-container runtime'); } diff --git a/src/subCommands/deploy/impl/function.ts b/src/subCommands/deploy/impl/function.ts index 482aa618..51821cb4 100644 --- a/src/subCommands/deploy/impl/function.ts +++ b/src/subCommands/deploy/impl/function.ts @@ -196,7 +196,7 @@ export default class Service extends Base { // custom-container 检查 s.yaml 中 image 是否存在 acr 中, 如果存在, 则弹出交互提示 // --skip-push 则不用提示 if (FC.isCustomContainerRuntime(this.local.runtime)) { - const { image } = this.local.customContainerConfig || {}; + const image = FC.getContainerImage(this.local); if (_.isNil(image)) { throw new Error('CustomContainerRuntime must have a valid image URL'); } @@ -256,7 +256,7 @@ export default class Service extends Base { logger.debug(`skip push is ${this.skipPush}`); return; } - const { image } = this.local.customContainerConfig || {}; + const image = FC.getContainerImage(this.local); if (_.isNil(image)) { throw new Error('CustomContainerRuntime must have a valid image URL'); } diff --git a/src/subCommands/local/impl/baseLocal.ts b/src/subCommands/local/impl/baseLocal.ts index 4cab6b03..5297dd45 100644 --- a/src/subCommands/local/impl/baseLocal.ts +++ b/src/subCommands/local/impl/baseLocal.ts @@ -215,7 +215,7 @@ export class BaseLocal { let image: string; if (this.isCustomContainerRuntime()) { - image = this.inputs.props.customContainerConfig.image; + image = FC.getContainerImage(this.inputs.props); logger.debug(`use fc docker CustomContainer image: ${image}`); } else if (fcDockerUseImage) { image = fcDockerUseImage; From 37181b2753018c68f8f744e554c5c8de5002933b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=B5=81?= Date: Mon, 24 Aug 2026 23:47:11 +0800 Subject: [PATCH 2/5] test: mock FC.getContainerImage in container image tests FC resource is automocked in these test files, so the newly added getContainerImage static always returns undefined, making _pushImage and getRuntimeBuildImage throw 'CustomContainerRuntime must have a valid image URL'. Mock getContainerImage explicitly per test to mirror the image the local config would resolve to. --- __tests__/ut/commands/build/impl/baseBuilder_test.ts | 2 ++ __tests__/ut/commands/deploy/impl/function_test.ts | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/__tests__/ut/commands/build/impl/baseBuilder_test.ts b/__tests__/ut/commands/build/impl/baseBuilder_test.ts index 1f4b6fb4..e50c3709 100644 --- a/__tests__/ut/commands/build/impl/baseBuilder_test.ts +++ b/__tests__/ut/commands/build/impl/baseBuilder_test.ts @@ -285,6 +285,7 @@ describe('Builder', () => { const builderWithCustomContainer = new TestBuilder(inputsWithCustomContainer); (FC.isCustomContainerRuntime as jest.Mock).mockReturnValue(true); + (FC.getContainerImage as jest.Mock).mockReturnValue('custom-image:latest'); const image = await builderWithCustomContainer.getRuntimeBuildImage(); expect(image).toBe('custom-image:latest'); @@ -300,6 +301,7 @@ describe('Builder', () => { const builderWithCustomContainer = new TestBuilder(inputsWithCustomContainer); (FC.isCustomContainerRuntime as jest.Mock).mockReturnValue(true); + (FC.getContainerImage as jest.Mock).mockReturnValue(''); (_.isEmpty as any).mockReturnValue(true); await expect(builderWithCustomContainer.getRuntimeBuildImage()).rejects.toThrow( diff --git a/__tests__/ut/commands/deploy/impl/function_test.ts b/__tests__/ut/commands/deploy/impl/function_test.ts index 172ff3d9..d3830585 100644 --- a/__tests__/ut/commands/deploy/impl/function_test.ts +++ b/__tests__/ut/commands/deploy/impl/function_test.ts @@ -442,6 +442,9 @@ describe('Service', () => { customContainerConfig: {}, } as IFunction; + // Mock FC.getContainerImage + (FC.getContainerImage as jest.Mock).mockReturnValue(undefined); + await expect((service as any)._pushImage()).rejects.toThrow( 'CustomContainerRuntime must have a valid image URL', ); @@ -455,6 +458,11 @@ describe('Service', () => { customContainerConfig: { image: 'registry.cn-hangzhou.aliyuncs.com/test/image' }, } as IFunction; + // Mock FC.getContainerImage + (FC.getContainerImage as jest.Mock).mockReturnValue( + 'registry.cn-hangzhou.aliyuncs.com/test/image', + ); + // Mock Acr.isAcrRegistry Acr.isAcrRegistry = jest.fn().mockReturnValue(true); @@ -475,6 +483,9 @@ describe('Service', () => { customContainerConfig: { image: 'docker.io/test/image' }, } as IFunction; + // Mock FC.getContainerImage + (FC.getContainerImage as jest.Mock).mockReturnValue('docker.io/test/image'); + // Mock Acr.isAcrRegistry Acr.isAcrRegistry = jest.fn().mockReturnValue(false); From c1866f68aed5f556814b735af50045e97fe42f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=B5=81?= Date: Wed, 26 Aug 2026 16:27:08 +0800 Subject: [PATCH 3/5] fix(local): honor microSandboxConfig.image in local container runs BaseLocal.isCustomContainerRuntime only matched 'custom-container', so getRuntimeRunImage skipped FC.getContainerImage for the micro-sandbox runtime and ignored microSandboxConfig.image. Align the predicate with FC.isCustomContainerRuntime so both container runtimes resolve the image, and add a regression test for getRuntimeRunImage with micro-sandbox. --- CLAUDE.md | 8 +++--- __tests__/ut/local/impl/baseLocal_test.ts | 30 +++++++++++++++++++++++ src/subCommands/local/impl/baseLocal.ts | 3 ++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fdf22485..301124cf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,10 +74,10 @@ See `docs/architecture.md` for detailed diagrams. Frozen scope (read-only for all subsequent work): -| Path | Contents | -| ---- | -------- | -| `src/subCommands/model/` | `model.ts`, `index.ts`, `fileManager.ts`, `constants.ts`, `utils/` | -| `src/commands-help/model.ts` | `model` command help text | +| Path | Contents | +| ---------------------------- | ------------------------------------------------------------------ | +| `src/subCommands/model/` | `model.ts`, `index.ts`, `fileManager.ts`, `constants.ts`, `utils/` | +| `src/commands-help/model.ts` | `model` command help text | Rules: diff --git a/__tests__/ut/local/impl/baseLocal_test.ts b/__tests__/ut/local/impl/baseLocal_test.ts index 2c758da6..9eec027d 100644 --- a/__tests__/ut/local/impl/baseLocal_test.ts +++ b/__tests__/ut/local/impl/baseLocal_test.ts @@ -7,6 +7,7 @@ import path from 'path'; import * as fs from 'fs-extra'; import { v4 as uuidV4 } from 'uuid'; import { getTempDir } from '../../../../src/utils'; +import FC from '../../../../src/resources/fc'; // Mock external dependencies jest.mock('../../../../src/logger', () => ({ @@ -210,6 +211,16 @@ describe('BaseLocal', () => { expect(result).toBe(true); }); + it('should return true for micro-sandbox runtime', () => { + const inputsWithMicroSandbox = JSON.parse(JSON.stringify(mockInputs)); + inputsWithMicroSandbox.props.runtime = 'micro-sandbox'; + + const instance = new BaseLocal(inputsWithMicroSandbox); + const result = instance.isCustomContainerRuntime(); + + expect(result).toBe(true); + }); + it('should return false for non custom-container runtime', () => { const instance = new BaseLocal(mockInputs); const result = instance.isCustomContainerRuntime(); @@ -218,6 +229,25 @@ describe('BaseLocal', () => { }); }); + describe('getRuntimeRunImage', () => { + it('should use microSandboxConfig.image for micro-sandbox runtime', async () => { + const inputsWithMicroSandbox = JSON.parse(JSON.stringify(mockInputs)); + inputsWithMicroSandbox.props.runtime = 'micro-sandbox'; + inputsWithMicroSandbox.props.microSandboxConfig = { image: 'registry/sandbox:v1' }; + + const instance = new BaseLocal(inputsWithMicroSandbox); + (FC.getContainerImage as jest.Mock).mockReturnValue('registry/sandbox:v1'); + + const image = await instance.getRuntimeRunImage(); + + expect(image).toBe('registry/sandbox:v1'); + expect(FC.getContainerImage).toHaveBeenCalledWith(inputsWithMicroSandbox.props); + expect(logger.debug).toHaveBeenCalledWith( + 'use fc docker CustomContainer image: registry/sandbox:v1', + ); + }); + }); + describe('checkCodeUri', () => { it('should return true when codeUri is valid', () => { const instance = new BaseLocal(mockInputs); diff --git a/src/subCommands/local/impl/baseLocal.ts b/src/subCommands/local/impl/baseLocal.ts index 5297dd45..4a717e0a 100644 --- a/src/subCommands/local/impl/baseLocal.ts +++ b/src/subCommands/local/impl/baseLocal.ts @@ -133,7 +133,8 @@ export class BaseLocal { } isCustomContainerRuntime(): boolean { - return this.inputs.props.runtime === 'custom-container'; + const runtime = this.inputs.props.runtime; + return runtime === 'custom-container' || runtime === 'micro-sandbox'; } // 判断是否开启rie的debug,只要使用了--debug或断点调试就开启。此时,不再打印result header中的日志。 From 3b03550eb7e4f4e27e533eec98abbea44ba1a2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=B5=81?= Date: Wed, 26 Aug 2026 18:30:21 +0800 Subject: [PATCH 4/5] style: apply f2elint fix to predicate destructuring --- src/subCommands/local/impl/baseLocal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subCommands/local/impl/baseLocal.ts b/src/subCommands/local/impl/baseLocal.ts index 4a717e0a..9918c4b4 100644 --- a/src/subCommands/local/impl/baseLocal.ts +++ b/src/subCommands/local/impl/baseLocal.ts @@ -133,7 +133,7 @@ export class BaseLocal { } isCustomContainerRuntime(): boolean { - const runtime = this.inputs.props.runtime; + const {runtime} = this.inputs.props; return runtime === 'custom-container' || runtime === 'micro-sandbox'; } From ab3aa2bb7005666f51840b0f759f4373f5c8b212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A5=BF=E6=B5=81?= Date: Wed, 26 Aug 2026 18:38:26 +0800 Subject: [PATCH 5/5] style: use bracket-spaced destructuring matching repo prettier --- src/subCommands/local/impl/baseLocal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subCommands/local/impl/baseLocal.ts b/src/subCommands/local/impl/baseLocal.ts index 9918c4b4..55ce0034 100644 --- a/src/subCommands/local/impl/baseLocal.ts +++ b/src/subCommands/local/impl/baseLocal.ts @@ -133,7 +133,7 @@ export class BaseLocal { } isCustomContainerRuntime(): boolean { - const {runtime} = this.inputs.props; + const { runtime } = this.inputs.props; return runtime === 'custom-container' || runtime === 'micro-sandbox'; }