From 6de841c4a1d3be9d59149f5c44df4b1621ad9344 Mon Sep 17 00:00:00 2001 From: liuzewen99 Date: Sat, 9 May 2026 15:15:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=20update=20=20@alicloud/fc20230330=20fr?= =?UTF-8?q?om=204.6.8=20-=204.7.4=E3=80=81add=20session=20pause=20and=20se?= =?UTF-8?q?ssion=20resume?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __tests__/it/integration_test.ts | 120 +++++++++ __tests__/ut/commands/session_test.ts | 342 +++++++++++++++++++++++++- package-lock.json | 15 +- package.json | 2 +- src/commands-help/session.ts | 48 +++- src/resources/fc/index.ts | 81 ++++++ src/subCommands/session/index.ts | 79 +++++- 7 files changed, 665 insertions(+), 22 deletions(-) diff --git a/__tests__/it/integration_test.ts b/__tests__/it/integration_test.ts index eb1fa044..cc0c878c 100644 --- a/__tests__/it/integration_test.ts +++ b/__tests__/it/integration_test.ts @@ -554,4 +554,124 @@ describe('Integration Tests', () => { expect(error).toBeDefined(); } }, 60000); + + test('session operations', async () => { + // Deploy function with sessionAffinity enabled (required for session operations) + const deployInputs = _.cloneDeep(inputs); + deployInputs.props.sessionAffinity = 'GENERATED_COOKIE'; + await myFcInstance.deploy(deployInputs); + + // Test create session + const createInputs = _.cloneDeep(inputs); + createInputs.command = 'session'; + createInputs.args = ['create', '--qualifier', 'LATEST', '--st', '3600', '--si', '1800']; + const createOutput = await myFcInstance.session(createInputs); + expect(createOutput).toBeDefined(); + expect(createOutput.sessionId).toBeDefined(); + const sessionId = createOutput.sessionId; + + // Test get session + const getInputs = _.cloneDeep(inputs); + getInputs.command = 'session'; + getInputs.args = ['get', '--session-id', sessionId, '--qualifier', 'LATEST']; + const getOutput = await myFcInstance.session(getInputs); + expect(getOutput).toBeDefined(); + expect(getOutput.sessionId).toBe(sessionId); + + // Test list sessions + const listInputs = _.cloneDeep(inputs); + listInputs.command = 'session'; + listInputs.args = ['list', '--qualifier', 'LATEST']; + const listOutput = await myFcInstance.session(listInputs); + expect(listOutput).toBeDefined(); + expect(Array.isArray(listOutput)).toBe(true); + + // Test update session + const updateInputs = _.cloneDeep(inputs); + updateInputs.command = 'session'; + updateInputs.args = [ + 'update', + '--session-id', + sessionId, + '--qualifier', + 'LATEST', + '--st', + '7200', + '--si', + '3600', + ]; + const updateOutput = await myFcInstance.session(updateInputs); + expect(updateOutput).toBeDefined(); + + // Test remove session (DELETE operation may return undefined) + const removeSessionInputs = _.cloneDeep(inputs); + removeSessionInputs.command = 'session'; + removeSessionInputs.args = ['remove', '--session-id', sessionId, '--qualifier', 'LATEST', '-y']; + await myFcInstance.session(removeSessionInputs); + + // Clean up: remove the function + const removeInputs = _.cloneDeep(inputs); + removeInputs.command = 'remove'; + removeInputs.args = ['--function', '--assume-yes']; + try { + await myFcInstance.remove(removeInputs); + } catch (error) { + // Ignore cleanup errors + } + }, 120000); + + test('session pause and resume', async () => { + // Deploy function with sessionAffinity enabled (required for session operations) + const deployInputs = _.cloneDeep(inputs); + deployInputs.props.sessionAffinity = 'GENERATED_COOKIE'; + await myFcInstance.deploy(deployInputs); + + // Create a session first + const createInputs = _.cloneDeep(inputs); + createInputs.command = 'session'; + createInputs.args = ['create', '--qualifier', 'LATEST', '--st', '3600', '--si', '1800']; + const createOutput = await myFcInstance.session(createInputs); + expect(createOutput).toBeDefined(); + const sessionId = createOutput.sessionId; + + try { + // Test pause - may fail if function is not SESSION_EXCLUSIVE with custom-container runtime + const pauseInputs = _.cloneDeep(inputs); + pauseInputs.command = 'session'; + pauseInputs.args = ['pause', '--session-id', sessionId, '--qualifier', 'LATEST']; + const pauseOutput = await myFcInstance.session(pauseInputs); + expect(pauseOutput).toBeDefined(); + + // If pause succeeded, test resume + const resumeInputs = _.cloneDeep(inputs); + resumeInputs.command = 'session'; + resumeInputs.args = ['resume', '--session-id', sessionId, '--qualifier', 'LATEST']; + const resumeOutput = await myFcInstance.session(resumeInputs); + expect(resumeOutput).toBeDefined(); + } catch (error) { + // Pause/resume requires SESSION_EXCLUSIVE + custom-container + healthCheck + // This is expected to fail on a basic python3.9 function + expect(error).toBeDefined(); + } + + // Clean up: remove the session + const removeSessionInputs = _.cloneDeep(inputs); + removeSessionInputs.command = 'session'; + removeSessionInputs.args = ['remove', '--session-id', sessionId, '--qualifier', 'LATEST', '-y']; + try { + await myFcInstance.session(removeSessionInputs); + } catch (error) { + // Ignore cleanup errors + } + + // Clean up: remove the function + const removeInputs = _.cloneDeep(inputs); + removeInputs.command = 'remove'; + removeInputs.args = ['--function', '--assume-yes']; + try { + await myFcInstance.remove(removeInputs); + } catch (error) { + // Ignore cleanup errors + } + }, 120000); }); diff --git a/__tests__/ut/commands/session_test.ts b/__tests__/ut/commands/session_test.ts index c4d777b7..661a8246 100644 --- a/__tests__/ut/commands/session_test.ts +++ b/__tests__/ut/commands/session_test.ts @@ -387,6 +387,163 @@ describe('Session', () => { 'qualifier not specified, please specify --qualifier', ); }); + + it('should propagate SDK error on get failure', async () => { + mockFcSdk.getFunctionSession = jest.fn().mockRejectedValue(new Error('API error')); + mockInputs.args = ['get', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.get()).rejects.toThrow('API error'); + expect(logger.error).toHaveBeenCalledWith('Failed to get session session-123: API error'); + }); + }); + + describe('pause', () => { + beforeEach(() => { + mockInputs.args = ['pause', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + }); + + it('should pause session successfully', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + sessionStatus: 'Paused', + }; + + mockFcSdk.pauseFunctionSession = jest.fn().mockResolvedValue(mockResult); + + const result = await session.pause(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.pauseFunctionSession).toHaveBeenCalledWith( + 'test-function', + 'session-123', + 'LATEST', + ); + }); + + it('should throw error when functionName is not specified', async () => { + delete mockInputs.props.functionName; + mockInputs.args = ['pause', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.pause()).rejects.toThrow( + 'functionName not specified, please specify --function-name', + ); + }); + + it('should throw error when sessionId is not specified', async () => { + mockInputs.args = ['pause', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.pause()).rejects.toThrow( + 'sessionId not specified, please specify --session-id', + ); + }); + + it('should throw error when qualifier is not specified', async () => { + mockInputs.args = ['pause', '--session-id', 'session-123']; + session = new Session(mockInputs); + + await expect(session.pause()).rejects.toThrow( + 'qualifier not specified, please specify --qualifier', + ); + }); + + it('should propagate SDK error on pause failure', async () => { + mockFcSdk.pauseFunctionSession = jest.fn().mockRejectedValue(new Error('API error')); + mockInputs.args = ['pause', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.pause()).rejects.toThrow('API error'); + expect(logger.error).toHaveBeenCalledWith('Failed to pause session session-123: API error'); + }); + }); + + describe('resume', () => { + beforeEach(() => { + mockInputs.args = ['resume', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + }); + + it('should resume session successfully', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + sessionStatus: 'Active', + }; + + mockFcSdk.resumeFunctionSession = jest.fn().mockResolvedValue(mockResult); + + const result = await session.resume(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.resumeFunctionSession).toHaveBeenCalledWith('test-function', 'session-123', { + qualifier: 'LATEST', + }); + }); + + it('should resume session with fileSystemOnly when provided', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + sessionStatus: 'Active', + }; + + mockFcSdk.resumeFunctionSession = jest.fn().mockResolvedValue(mockResult); + mockInputs.args = [ + 'resume', + '--session-id', + 'session-123', + '--qualifier', + 'LATEST', + '--file-system-only', + ]; + session = new Session(mockInputs); + + const result = await session.resume(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.resumeFunctionSession).toHaveBeenCalledWith('test-function', 'session-123', { + qualifier: 'LATEST', + fileSystemOnly: true, + }); + }); + + it('should throw error when functionName is not specified', async () => { + delete mockInputs.props.functionName; + mockInputs.args = ['resume', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.resume()).rejects.toThrow( + 'functionName not specified, please specify --function-name', + ); + }); + + it('should throw error when sessionId is not specified', async () => { + mockInputs.args = ['resume', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.resume()).rejects.toThrow( + 'sessionId not specified, please specify --session-id', + ); + }); + + it('should throw error when qualifier is not specified', async () => { + mockInputs.args = ['resume', '--session-id', 'session-123']; + session = new Session(mockInputs); + + await expect(session.resume()).rejects.toThrow( + 'qualifier not specified, please specify --qualifier', + ); + }); + + it('should propagate SDK error on resume failure', async () => { + mockFcSdk.resumeFunctionSession = jest.fn().mockRejectedValue(new Error('API error')); + mockInputs.args = ['resume', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.resume()).rejects.toThrow('API error'); + expect(logger.error).toHaveBeenCalledWith('Failed to resume session session-123: API error'); + }); }); describe('update', () => { @@ -477,10 +634,44 @@ describe('Session', () => { session = new Session(mockInputs); await expect(session.update()).rejects.toThrow( - 'timeout must be a number between 0 and 21600', + 'sessionTTLInSeconds must be a number between 0 and 21600', + ); + }); + + it('should throw error when sessionIdleTimeoutInSeconds is invalid', async () => { + mockInputs.args = [ + 'update', + '--session-id', + 'session-123', + '--qualifier', + 'LATEST', + '--session-idle-timeout-in-seconds', + '99999', + ]; + session = new Session(mockInputs); + + await expect(session.update()).rejects.toThrow( + 'sessionIdleTimeoutInSeconds must be a number between 0 and 21600', ); }); + it('should update session without timeout fields when not specified', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + }; + + mockFcSdk.updateFunctionSession = jest.fn().mockResolvedValue(mockResult); + mockInputs.args = ['update', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + const result = await session.update(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.updateFunctionSession).toHaveBeenCalledWith('test-function', 'session-123', { + qualifier: 'LATEST', + }); + }); + it('should update session with disableSessionIdReuse when provided', async () => { const mockResult = { sessionId: 'session-123', @@ -514,6 +705,146 @@ describe('Session', () => { disableSessionIdReuse: true, }); }); + + it('should update session with nasConfig when provided', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + sessionTTLInSeconds: 7200, + sessionIdleTimeoutInSeconds: 3600, + }; + + mockFcSdk.updateFunctionSession = jest.fn().mockResolvedValue(mockResult); + mockInputs.args = [ + 'update', + '--session-id', + 'session-123', + '--qualifier', + 'LATEST', + '--session-ttl-in-seconds', + '7200', + '--session-idle-timeout-in-seconds', + '3600', + '--nas-config', + '{"userId":1000,"groupId":1000,"mountPoints":[{"serverAddr":"nas-server-addr","mountDir":"/mnt/nas","enableTLS":true}]}', + ]; + session = new Session(mockInputs); + + const result = await session.update(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.updateFunctionSession).toHaveBeenCalledWith('test-function', 'session-123', { + qualifier: 'LATEST', + sessionTTLInSeconds: 7200, + sessionIdleTimeoutInSeconds: 3600, + nasConfig: { + userId: 1000, + groupId: 1000, + mountPoints: [ + { + serverAddr: 'nas-server-addr', + mountDir: '/mnt/nas', + enableTLS: true, + }, + ], + }, + }); + }); + + it('should update session with ossMountConfig when provided', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + sessionTTLInSeconds: 7200, + sessionIdleTimeoutInSeconds: 3600, + }; + + mockFcSdk.updateFunctionSession = jest.fn().mockResolvedValue(mockResult); + mockInputs.args = [ + 'update', + '--session-id', + 'session-123', + '--qualifier', + 'LATEST', + '--session-ttl-in-seconds', + '7200', + '--session-idle-timeout-in-seconds', + '3600', + '--oss-mount-config', + '{"mountPoints":[{"bucketName":"test-bucket","bucketPath":"cn-hangzhou","mountDir":"/mnt/oss","readOnly":false}]}', + ]; + session = new Session(mockInputs); + + const result = await session.update(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.updateFunctionSession).toHaveBeenCalledWith('test-function', 'session-123', { + qualifier: 'LATEST', + sessionTTLInSeconds: 7200, + sessionIdleTimeoutInSeconds: 3600, + ossMountConfig: { + mountPoints: [ + { + bucketName: 'test-bucket', + bucketPath: 'cn-hangzhou', + mountDir: '/mnt/oss', + readOnly: false, + }, + ], + }, + }); + }); + + it('should update session with polarFsConfig when provided', async () => { + const mockResult = { + sessionId: 'session-123', + qualifier: 'LATEST', + sessionTTLInSeconds: 7200, + sessionIdleTimeoutInSeconds: 3600, + }; + + mockFcSdk.updateFunctionSession = jest.fn().mockResolvedValue(mockResult); + mockInputs.args = [ + 'update', + '--session-id', + 'session-123', + '--qualifier', + 'LATEST', + '--session-ttl-in-seconds', + '7200', + '--session-idle-timeout-in-seconds', + '3600', + '--polar-fs-config', + '{"userId":1000,"groupId":1000,"mountPoints":[{"instanceId":"pfs-test","mountDir":"/mnt/polar","remoteDir":"/"}]}', + ]; + session = new Session(mockInputs); + + const result = await session.update(); + expect(result).toEqual(mockResult); + expect(mockFcSdk.updateFunctionSession).toHaveBeenCalledWith('test-function', 'session-123', { + qualifier: 'LATEST', + sessionTTLInSeconds: 7200, + sessionIdleTimeoutInSeconds: 3600, + polarFsConfig: { + userId: 1000, + groupId: 1000, + mountPoints: [ + { + instanceId: 'pfs-test', + mountDir: '/mnt/polar', + remoteDir: '/', + }, + ], + }, + }); + }); + + it('should propagate SDK error on update failure', async () => { + mockFcSdk.updateFunctionSession = jest.fn().mockRejectedValue(new Error('API error')); + mockInputs.args = ['update', '--session-id', 'session-123', '--qualifier', 'LATEST']; + session = new Session(mockInputs); + + await expect(session.update()).rejects.toThrow('API error'); + expect(logger.error).toHaveBeenCalledWith('Failed to update session session-123: API error'); + }); }); describe('list', () => { @@ -587,6 +918,15 @@ describe('Session', () => { 'functionName not specified, please specify --function-name', ); }); + + it('should propagate SDK error on list failure', async () => { + mockFcSdk.listFunctionSessions = jest.fn().mockRejectedValue(new Error('API error')); + mockInputs.args = ['list']; + session = new Session(mockInputs); + + await expect(session.list()).rejects.toThrow('API error'); + expect(logger.error).toHaveBeenCalledWith('Failed to list sessions: API error'); + }); }); describe('remove', () => { diff --git a/package-lock.json b/package-lock.json index 569bea99..604ddee6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@alicloud/devs20230714": "^2.5.0", "@alicloud/fc2": "^2.6.6", - "@alicloud/fc20230330": "4.6.8", + "@alicloud/fc20230330": "4.7.4", "@alicloud/pop-core": "^1.8.0", "@serverless-cd/srm-aliyun-oss": "^0.0.1-beta.8", "@serverless-cd/srm-aliyun-pop-core": "^0.0.8-beta.1", @@ -213,9 +213,10 @@ } }, "node_modules/@alicloud/fc20230330": { - "version": "4.6.8", - "resolved": "https://registry.npmjs.org/@alicloud/fc20230330/-/fc20230330-4.6.8.tgz", - "integrity": "sha512-RNgaPYr9SOCn3tjB5KxKnmawbc6IGjOhDmrWGyWSjGHRFoX/uCKS/eZJM2twyyqiC1XauAfNWSS6Ax9uhJB0kQ==", + "version": "4.7.4", + "resolved": "https://registry.npmmirror.com/@alicloud/fc20230330/-/fc20230330-4.7.4.tgz", + "integrity": "sha512-DkxiA9JT6e2tRdFx05Pub+vQxwgoPJK6ZbDB3ZLs1b+wSkYhU40t49EhlA8deBhm+shTpCrG3J0vdme0ZHCB1A==", + "license": "Apache-2.0", "dependencies": { "@alicloud/openapi-core": "^1.0.0", "@darabonba/typescript": "^1.0.0" @@ -15707,9 +15708,9 @@ } }, "@alicloud/fc20230330": { - "version": "4.6.8", - "resolved": "https://registry.npmjs.org/@alicloud/fc20230330/-/fc20230330-4.6.8.tgz", - "integrity": "sha512-RNgaPYr9SOCn3tjB5KxKnmawbc6IGjOhDmrWGyWSjGHRFoX/uCKS/eZJM2twyyqiC1XauAfNWSS6Ax9uhJB0kQ==", + "version": "4.7.4", + "resolved": "https://registry.npmmirror.com/@alicloud/fc20230330/-/fc20230330-4.7.4.tgz", + "integrity": "sha512-DkxiA9JT6e2tRdFx05Pub+vQxwgoPJK6ZbDB3ZLs1b+wSkYhU40t49EhlA8deBhm+shTpCrG3J0vdme0ZHCB1A==", "requires": { "@alicloud/openapi-core": "^1.0.0", "@darabonba/typescript": "^1.0.0" diff --git a/package.json b/package.json index cd1ad661..afb14869 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "dependencies": { "@alicloud/devs20230714": "^2.5.0", "@alicloud/fc2": "^2.6.6", - "@alicloud/fc20230330": "4.6.8", + "@alicloud/fc20230330": "4.7.4", "@alicloud/pop-core": "^1.8.0", "@serverless-cd/srm-aliyun-oss": "^0.0.1-beta.8", "@serverless-cd/srm-aliyun-pop-core": "^0.0.8-beta.1", diff --git a/src/commands-help/session.ts b/src/commands-help/session.ts index 509f486c..49b2feb8 100644 --- a/src/commands-help/session.ts +++ b/src/commands-help/session.ts @@ -78,6 +78,44 @@ Examples with CLI: ], }, }, + pause: { + help: { + description: `Pause a session. + +Examples with CLI: + $ s cli fc3 session pause --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST -a default`, + summary: 'Pause a session', + option: [ + [ + '--region ', + '[C-Required] Specify fc region, you can see all supported regions in https://help.aliyun.com/document_detail/2512917.html', + ], + ['--function-name ', '[C-Required] Specify function name'], + ['--session-id ', '[Required] Specify session id'], + ['--qualifier ', '[Required] Specify the qualifier parameter'], + ], + }, + }, + resume: { + help: { + description: `Resume a paused session. + +Examples with CLI: + $ s cli fc3 session resume --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST -a default + $ s cli fc3 session resume --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --file-system-only -a default`, + summary: 'Resume a paused session', + option: [ + [ + '--region ', + '[C-Required] Specify fc region, you can see all supported regions in https://help.aliyun.com/document_detail/2512917.html', + ], + ['--function-name ', '[C-Required] Specify function name'], + ['--session-id ', '[Required] Specify session id'], + ['--qualifier ', '[Required] Specify the qualifier parameter'], + ['--file-system-only', '[Optional] Only resume the file system'], + ], + }, + }, remove: { help: { description: `Remove a session. @@ -104,7 +142,10 @@ Examples with CLI: Examples with CLI: $ s cli fc3 session update --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --session-ttl-in-seconds 900 -a default - $ s cli fc3 session update --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --disable-session-id-reuse -a default`, + $ s cli fc3 session update --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --disable-session-id-reuse -a default + $ s cli fc3 session update --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --nas-config '{"userId": 1000, "groupId": 1000, "mountPoints": [{"serverAddr": "example.nas.aliyuncs.com:/", "mountDir": "/mnt/nas", "enableTLS": true}]}' -a default + $ s cli fc3 session update --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --oss-mount-config '{"mountPoints": [{"bucketName": "my-bucket", "bucketPath": "cn-hangzhou", "mountDir": "/mnt/oss", "readOnly": false}]}' -a default + $ s cli fc3 session update --region cn-hangzhou --function-name my-function --session-id session-123 --qualifier LATEST --polar-fs-config '{"userId": 1000, "groupId": 1000, "mountPoints": [{"instanceId": "pc-xxx", "mountDir": "/mnt/polar", "remoteDir": "/"}]}' -a default`, summary: 'Update a session', option: [ [ @@ -116,13 +157,16 @@ Examples with CLI: ['--qualifier ', '[Required] Specify the qualifier parameter'], [ '--st, --session-ttl-in-seconds ', - '[Optional] Session TTL in seconds, between 0 and 2512917', + '[Optional] Session TTL in seconds, between 0 and 21600', ], [ '--si, --session-idle-timeout-in-seconds ', '[Optional] Session idle timeout in seconds, between 0 and 21600', ], ['--dsr, --disable-session-id-reuse', '[Optional] Disable session ID reuse'], + ['--nas-config ', '[Optional] Update the nasConfig.'], + ['--oss-mount-config ', '[Optional] Update the ossMountConfig.'], + ['--polar-fs-config ', '[Optional] Update the polarFsConfig.'], ], }, }, diff --git a/src/resources/fc/index.ts b/src/resources/fc/index.ts index 22d5dcd4..5c14551b 100644 --- a/src/resources/fc/index.ts +++ b/src/resources/fc/index.ts @@ -26,6 +26,8 @@ import { DeleteSessionRequest, GetSessionRequest, ListSessionsRequest, + PauseSessionRequest, + ResumeSessionRequest, UpdateSessionRequest, UpdateSessionInput, ChangeResourceGroupRequest, @@ -1052,8 +1054,62 @@ export default class FC extends FC_Client { } async updateFunctionSession(functionName: string, sessionId: string, config: any) { + let nasConfig: NASConfig | undefined; + if (config.nasConfig && !_.isEmpty(config.nasConfig.mountPoints)) { + const mountPoints = config.nasConfig.mountPoints.map( + (mountPoint: any) => + new NASMountConfig({ + enableTLS: mountPoint.enableTLS, + mountDir: mountPoint.mountDir, + serverAddr: mountPoint.serverAddr, + }), + ); + nasConfig = new NASConfig({ + groupId: config.nasConfig.groupId, + mountPoints, + userId: config.nasConfig.userId, + }); + } + + let ossMountConfig: OSSMountConfig | undefined; + if (config.ossMountConfig && !_.isEmpty(config.ossMountConfig.mountPoints)) { + const mountPoints = config.ossMountConfig.mountPoints.map( + (mountPoint: any) => + new OSSMountPoint({ + bucketName: mountPoint.bucketName, + bucketPath: mountPoint.bucketPath, + endpoint: mountPoint.endpoint, + mountDir: mountPoint.mountDir, + readOnly: mountPoint.readOnly, + }), + ); + ossMountConfig = new OSSMountConfig({ + mountPoints, + }); + } + + let polarFsConfig: PolarFsConfig | undefined; + if (config.polarFsConfig && !_.isEmpty(config.polarFsConfig.mountPoints)) { + const mountPoints = config.polarFsConfig.mountPoints.map( + (mountPoint: any) => + new PolarFsMountConfig({ + instanceId: mountPoint.instanceId, + mountDir: mountPoint.mountDir, + remoteDir: mountPoint.remoteDir, + }), + ); + polarFsConfig = new PolarFsConfig({ + groupId: config.polarFsConfig.groupId, + mountPoints, + userId: config.polarFsConfig.userId, + }); + } + const updateSessionInput = new UpdateSessionInput({ disableSessionIdReuse: config.disableSessionIdReuse, + nasConfig, + ossMountConfig, + polarFsConfig, sessionTTLInSeconds: config.sessionTTLInSeconds, sessionIdleTimeoutInSeconds: config.sessionIdleTimeoutInSeconds, }); @@ -1097,6 +1153,31 @@ export default class FC extends FC_Client { return body; } + async pauseFunctionSession(functionName: string, sessionId: string, qualifier: string) { + const pauseSessionRequest = new PauseSessionRequest({ qualifier }); + const response = await this.fc20230330Client.pauseSession( + functionName, + sessionId, + pauseSessionRequest, + ); + const { body } = response.toMap(); + return body; + } + + async resumeFunctionSession(functionName: string, sessionId: string, config: any) { + const resumeSessionRequest = new ResumeSessionRequest({ + qualifier: config.qualifier, + fileSystemOnly: config.fileSystemOnly, + }); + const response = await this.fc20230330Client.resumeSession( + functionName, + sessionId, + resumeSessionRequest, + ); + const { body } = response.toMap(); + return body; + } + tagsToLowerCase(tags) { if (!tags) { return []; diff --git a/src/subCommands/session/index.ts b/src/subCommands/session/index.ts index d1128708..d5eb616c 100644 --- a/src/subCommands/session/index.ts +++ b/src/subCommands/session/index.ts @@ -18,6 +18,7 @@ export default class Session { private nasConfig: any; private ossMountConfig: any; private polarFsConfig: any; + private fileSystemOnly: boolean; constructor(readonly inputs: IInputs) { const opts = parseArgv(inputs.args, { @@ -29,8 +30,9 @@ export default class Session { 'disable-session-id-reuse': 'dsr', 'oss-mount-config': 'omc', 'polar-fs-config': 'pfc', + 'file-system-only': 'fso', }, - boolean: ['help', 'y', 'disable-session-id-reuse'], + boolean: ['help', 'y', 'disable-session-id-reuse', 'file-system-only'], string: [ 'region', 'function-name', @@ -65,6 +67,7 @@ export default class Session { this.subCommand = subCommand; this.yes = !!yes; + this.fileSystemOnly = !!opts['file-system-only']; const userAgent = getUserAgent(inputs.userAgent, `session`); this.fcSdk = new FC(this.region, this.inputs.credential, { endpoint: inputs.props.endpoint, @@ -170,14 +173,65 @@ export default class Session { } } + async pause() { + const sessionId = this.opts['session-id']; + const { qualifier } = this.opts; + if (_.isEmpty(this.functionName)) { + throw new Error('functionName not specified, please specify --function-name'); + } + if (_.isEmpty(sessionId)) { + throw new Error('sessionId not specified, please specify --session-id'); + } + if (_.isEmpty(qualifier)) { + throw new Error('qualifier not specified, please specify --qualifier'); + } + + try { + const result = await this.fcSdk.pauseFunctionSession(this.functionName, sessionId, qualifier); + logger.info(`Session paused successfully: ${JSON.stringify(result)}`); + return result; + } catch (ex) { + logger.error(`Failed to pause session ${sessionId}: ${ex.message}`); + throw ex; + } + } + + async resume() { + const sessionId = this.opts['session-id']; + const { qualifier } = this.opts; + if (_.isEmpty(this.functionName)) { + throw new Error('functionName not specified, please specify --function-name'); + } + if (_.isEmpty(sessionId)) { + throw new Error('sessionId not specified, please specify --session-id'); + } + if (_.isEmpty(qualifier)) { + throw new Error('qualifier not specified, please specify --qualifier'); + } + + const config: any = { qualifier }; + if (this.fileSystemOnly) config.fileSystemOnly = this.fileSystemOnly; + + try { + const result = await this.fcSdk.resumeFunctionSession(this.functionName, sessionId, config); + logger.info(`Session resumed successfully: ${JSON.stringify(result)}`); + return result; + } catch (ex) { + logger.error(`Failed to resume session ${sessionId}: ${ex.message}`); + throw ex; + } + } + async update() { const sessionId = this.opts['session-id']; const { qualifier } = this.opts; const disableSessionIdReuse = this.opts['disable-session-id-reuse']; - const sessionTTLInSeconds = parseInt(this.opts['session-ttl-in-seconds'], 10); + const sessionTTLInSeconds = this.opts['session-ttl-in-seconds'] + ? parseInt(this.opts['session-ttl-in-seconds'], 10) + : undefined; const sessionIdleTimeoutInSeconds = this.opts['session-idle-timeout-in-seconds'] ? parseInt(this.opts['session-idle-timeout-in-seconds'], 10) - : 1800; + : undefined; if (_.isEmpty(this.functionName)) { throw new Error('functionName not specified, please specify --function-name'); } @@ -188,18 +242,18 @@ export default class Session { throw new Error('qualifier not specified, please specify --qualifier'); } if ( - !_.isNumber(sessionTTLInSeconds) || - sessionTTLInSeconds < 0 || - sessionTTLInSeconds > 21600 + sessionTTLInSeconds !== undefined && + (!_.isNumber(sessionTTLInSeconds) || sessionTTLInSeconds < 0 || sessionTTLInSeconds > 21600) ) { - throw new Error('timeout must be a number between 0 and 21600'); + throw new Error('sessionTTLInSeconds must be a number between 0 and 21600'); } if ( - !_.isNumber(sessionIdleTimeoutInSeconds) || - sessionIdleTimeoutInSeconds < 0 || - sessionIdleTimeoutInSeconds > 21600 + sessionIdleTimeoutInSeconds !== undefined && + (!_.isNumber(sessionIdleTimeoutInSeconds) || + sessionIdleTimeoutInSeconds < 0 || + sessionIdleTimeoutInSeconds > 21600) ) { - throw new Error('timeout must be a number between 0 and 21600'); + throw new Error('sessionIdleTimeoutInSeconds must be a number between 0 and 21600'); } const config: any = {}; @@ -208,6 +262,9 @@ export default class Session { if (sessionTTLInSeconds) config.sessionTTLInSeconds = sessionTTLInSeconds; if (sessionIdleTimeoutInSeconds) config.sessionIdleTimeoutInSeconds = sessionIdleTimeoutInSeconds; + if (this.nasConfig) config.nasConfig = this.nasConfig; + if (this.ossMountConfig) config.ossMountConfig = this.ossMountConfig; + if (this.polarFsConfig) config.polarFsConfig = this.polarFsConfig; try { const result = await this.fcSdk.updateFunctionSession(this.functionName, sessionId, config);