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/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); 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/__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..55ce0034 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; + return runtime === 'custom-container' || runtime === 'micro-sandbox'; } // 判断是否开启rie的debug,只要使用了--debug或断点调试就开启。此时,不再打印result header中的日志。 @@ -215,7 +216,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;