diff --git a/__tests__/ut/commands/deploy/deploy_test.ts b/__tests__/ut/commands/deploy/deploy_test.ts index 14651f30..e5945063 100644 --- a/__tests__/ut/commands/deploy/deploy_test.ts +++ b/__tests__/ut/commands/deploy/deploy_test.ts @@ -86,6 +86,7 @@ describe('Deploy', () => { 'async-invoke-config': undefined, 'assume-yes': false, 'skip-push': false, + 'skip-acceleration-wait': false, }); // Mock verify @@ -202,7 +203,7 @@ describe('Deploy', () => { alias: { 'assume-yes': 'y', }, - boolean: ['skip-push', 'async_invoke_config'], + boolean: ['skip-push', 'async_invoke_config', 'skip-acceleration-wait'], }); }); @@ -213,6 +214,7 @@ describe('Deploy', () => { 'async-invoke-config': undefined, 'assume-yes': true, 'skip-push': true, + 'skip-acceleration-wait': true, }); new Deploy(mockInputs); @@ -221,6 +223,27 @@ describe('Deploy', () => { type: 'function', yes: true, skipPush: true, + skipAccelerationWait: true, + }); + }); + + it('should pass skipAccelerationWait as undefined when not specified', () => { + (parseArgv as jest.Mock).mockReturnValue({ + function: 'function', + trigger: undefined, + 'async-invoke-config': undefined, + 'assume-yes': true, + 'skip-push': false, + 'skip-acceleration-wait': false, + }); + + new Deploy(mockInputs); + + expect(Service).toHaveBeenCalledWith(mockInputs, { + type: 'function', + yes: true, + skipPush: false, + skipAccelerationWait: false, }); }); diff --git a/__tests__/ut/commands/deploy/impl/function_test.ts b/__tests__/ut/commands/deploy/impl/function_test.ts index 34edbbf8..172ff3d9 100644 --- a/__tests__/ut/commands/deploy/impl/function_test.ts +++ b/__tests__/ut/commands/deploy/impl/function_test.ts @@ -86,6 +86,7 @@ describe('Service', () => { expect(service.type).toBeUndefined(); expect(service.skipPush).toBeUndefined(); + expect(service.skipAccelerationWait).toBeUndefined(); expect(service.local).toEqual({ functionName: 'test-function', runtime: 'nodejs12', @@ -237,10 +238,75 @@ describe('Service', () => { { slsAuto: false, type: 'config', + skipAccelerationWait: undefined, }, ); }); + it('should pass skipAccelerationWait to deployFunction', async () => { + service = new Service(mockInputs, { ...mockOpts, skipAccelerationWait: true }); + service.needDeploy = true; + Object.defineProperty(service, 'type', { + value: 'config', + writable: true, + }); + + const mockFcSdk = { + deployFunction: jest.fn().mockResolvedValue(undefined), + }; + Object.defineProperty(service, 'fcSdk', { + value: mockFcSdk, + writable: true, + }); + + jest.spyOn(service as any, '_deployAuto').mockResolvedValue(undefined); + jest.spyOn(service as any, '_uploadCode').mockResolvedValue(true); + + await service.run(); + + expect(service.fcSdk.deployFunction).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + skipAccelerationWait: true, + }), + ); + }); + + it('should pass skipAccelerationWait=true with type=code', async () => { + service = new Service(mockInputs, { ...mockOpts, skipAccelerationWait: true }); + service.needDeploy = true; + Object.defineProperty(service, 'type', { + value: 'code', + writable: true, + }); + + const mockFcSdk = { + deployFunction: jest.fn().mockResolvedValue(undefined), + uploadCodeToTmpOss: jest + .fn() + .mockResolvedValue({ ossBucketName: 'bucket', ossObjectName: 'object' }), + }; + Object.defineProperty(service, 'fcSdk', { + value: mockFcSdk, + writable: true, + }); + + jest.spyOn(service as any, '_uploadCode').mockImplementation(async function (this: any) { + this.local.code = { ossBucketName: 'bucket', ossObjectName: 'object' }; + return true; + }); + + await service.run(); + + expect(service.fcSdk.deployFunction).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + skipAccelerationWait: true, + type: 'code', + }), + ); + }); + it('should upload code when type is not config', async () => { service = new Service(mockInputs, mockOpts); service.needDeploy = true; @@ -308,6 +374,7 @@ describe('Service', () => { { slsAuto: false, type: 'code', + skipAccelerationWait: undefined, }, ); }); diff --git a/__tests__/ut/resources/fc/index_test.ts b/__tests__/ut/resources/fc/index_test.ts index fee06da2..5ad24ed4 100644 --- a/__tests__/ut/resources/fc/index_test.ts +++ b/__tests__/ut/resources/fc/index_test.ts @@ -131,6 +131,58 @@ describe('FC', () => { expect(mockFc20230330Client.getFunction).toHaveBeenCalled(); }); + it('should skip waiting when skipAccelerationWait is true for custom container runtime', async () => { + const logger = require('../../../../src/logger'); + + await fc.untilFunctionStateOK(mockConfig, 'CREATE', true); + + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Skip waiting for test-image:latest optimization'), + ); + expect(mockFc20230330Client.getFunction).not.toHaveBeenCalled(); + }); + + it('should skip waiting when skipAccelerationWait is true for UPDATE reason', async () => { + const logger = require('../../../../src/logger'); + + await fc.untilFunctionStateOK(mockConfig, 'UPDATE', true); + + expect(logger.info).toHaveBeenCalledWith( + expect.stringContaining('Skip waiting for test-image:latest optimization'), + ); + expect(mockFc20230330Client.getFunction).not.toHaveBeenCalled(); + }); + + it('should wait normally when skipAccelerationWait is false for custom container runtime', async () => { + const mockFunctionMeta = { + state: 'Active', + lastUpdateStatus: 'Success', + }; + + mockFc20230330Client.getFunction.mockResolvedValue({ + toMap: () => ({ body: mockFunctionMeta }), + }); + + await fc.untilFunctionStateOK(mockConfig, 'CREATE', false); + + expect(mockFc20230330Client.getFunction).toHaveBeenCalled(); + }); + + it('should wait normally when skipAccelerationWait is undefined for custom container runtime', async () => { + const mockFunctionMeta = { + state: 'Active', + lastUpdateStatus: 'Success', + }; + + mockFc20230330Client.getFunction.mockResolvedValue({ + toMap: () => ({ body: mockFunctionMeta }), + }); + + await fc.untilFunctionStateOK(mockConfig, 'CREATE'); + + expect(mockFc20230330Client.getFunction).toHaveBeenCalled(); + }); + it('should not wait for non-custom container runtime', async () => { // Mock FC static method const FC = require('../../../../src/resources/fc').default; @@ -174,6 +226,7 @@ describe('FC', () => { await fc.deployFunction(mockConfig, { slsAuto: false, type: undefined, + skipAccelerationWait: undefined, }); expect(mockFc20230330Client.untagResources).toHaveBeenCalled(); @@ -187,6 +240,7 @@ describe('FC', () => { fc.deployFunction(mockConfig, { slsAuto: false, type: undefined, + skipAccelerationWait: undefined, }), ).rejects.toThrow('The number of tags cannot exceed 20'); }); @@ -201,6 +255,7 @@ describe('FC', () => { fc.deployFunction(mockConfig, { slsAuto: false, type: undefined, + skipAccelerationWait: undefined, }), ).rejects.toThrow('The tag keys must be unique'); }); diff --git a/src/commands-help/deploy.ts b/src/commands-help/deploy.ts index d49752ee..5ed13a9a 100644 --- a/src/commands-help/deploy.ts +++ b/src/commands-help/deploy.ts @@ -13,6 +13,10 @@ Examples: option: [ ['-y, --assume-yes', "[Optional] Don't ask, delete directly"], ['--skip-push', '[Optional] Specify if skip automatically pushing docker container images'], + [ + '--skip-acceleration-wait', + '[Optional] Specify if skip waiting for image acceleration after deploy', + ], [ "--function ['code'/'config']", "[Optional] Only deploy function configuration or code. Use 'code' to deploy function code only, use 'config' to deploy function configuration only", diff --git a/src/resources/fc/index.ts b/src/resources/fc/index.ts index 5c14551b..bd549649 100644 --- a/src/resources/fc/index.ts +++ b/src/resources/fc/index.ts @@ -81,7 +81,7 @@ export default class FC extends FC_Client { static isCustomRuntime = isCustomRuntime; static replaceFunctionConfig = replaceFunctionConfig; - async untilFunctionStateOK(config: IFunction, reason: string) { + async untilFunctionStateOK(config: IFunction, reason: string, skipAccelerationWait?: boolean) { const retryInterval = 2; const startTime = new Date().getTime(); const calculateRetryTime = (minute: number) => @@ -93,6 +93,12 @@ export default class FC extends FC_Client { const retryContainerAccelerated = FC.isCustomContainerRuntime(config.runtime); // 部署镜像需要重试 3min, 直到达到!(State == Pending || LastUpdateStatus == InProgress) if (retryContainerAccelerated) { + 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.`, + ); + return; + } console.log(''); if (reason === 'CREATE') { if (isAppCenter()) { @@ -183,7 +189,7 @@ export default class FC extends FC_Client { /** * 创建或者修改函数 */ - async deployFunction(config: IFunction, { slsAuto, type }): Promise { + async deployFunction(config: IFunction, { slsAuto, type, skipAccelerationWait }): Promise { logger.debug(`Deploy function use config:\n${JSON.stringify(config, null, 2)}`); let needUpdate = false; let remoteConfig = null; @@ -217,7 +223,7 @@ export default class FC extends FC_Client { logger.debug(`Need create function ${config.functionName}`); try { await this.createFunction(config); - await this.untilFunctionStateOK(config, 'CREATE'); + await this.untilFunctionStateOK(config, 'CREATE', skipAccelerationWait); return; } catch (ex) { logger.debug(`Create function error: ${ex.message}`); @@ -291,7 +297,7 @@ export default class FC extends FC_Client { _.unset(config, 'customContainerConfig'); } await this.updateFunction(config); - await this.untilFunctionStateOK(config, 'UPDATE'); + await this.untilFunctionStateOK(config, 'UPDATE', skipAccelerationWait); if (config.resourceGroupId) { const remoteResourceGroupId = remoteConfig?.body?.resourceGroupId; if (remoteResourceGroupId !== config.resourceGroupId) { diff --git a/src/subCommands/deploy/impl/function.ts b/src/subCommands/deploy/impl/function.ts index 340bc285..a613ea22 100644 --- a/src/subCommands/deploy/impl/function.ts +++ b/src/subCommands/deploy/impl/function.ts @@ -33,11 +33,13 @@ interface IOpts { type?: IType; yes?: boolean; skipPush?: boolean; + skipAccelerationWait?: boolean; } export default class Service extends Base { readonly type?: IType; readonly skipPush?: boolean = false; + readonly skipAccelerationWait?: boolean = false; remote?: any; local: IFunction; @@ -54,6 +56,7 @@ export default class Service extends Base { this.type = opts.type; this.skipPush = opts.skipPush; + this.skipAccelerationWait = opts.skipAccelerationWait; logger.debug(`deploy function type: ${this.type}`); this.local = _.cloneDeep(inputs.props); @@ -131,6 +134,7 @@ export default class Service extends Base { await this.fcSdk.deployFunction(config, { slsAuto: !_.isEmpty(this.createResource.sls), type: this.type, + skipAccelerationWait: this.skipAccelerationWait, }); return this.needDeploy; } diff --git a/src/subCommands/deploy/index.ts b/src/subCommands/deploy/index.ts index 571dd62d..67c27b67 100644 --- a/src/subCommands/deploy/index.ts +++ b/src/subCommands/deploy/index.ts @@ -36,7 +36,7 @@ export default class Deploy { alias: { 'assume-yes': 'y', }, - boolean: ['skip-push', 'async_invoke_config'], + boolean: ['skip-push', 'async_invoke_config', 'skip-acceleration-wait'], }); // TODO: 更完善的验证 @@ -53,6 +53,7 @@ export default class Deploy { 'async-invoke-config': async_invoke_config, 'assume-yes': yes, 'skip-push': skipPush, + 'skip-acceleration-wait': skipAccelerationWait, } = this.opts; logger.debug('parse argv:'); logger.debug(this.opts); @@ -64,6 +65,7 @@ export default class Deploy { type, yes, skipPush, + skipAccelerationWait, }); // function } if (deployAll || trigger) {