diff --git a/docs/swagger.yaml b/docs/swagger.yaml index c3da65dc..c36985ad 100755 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1973,6 +1973,202 @@ paths: description: Duplicate Name "500": description: Internal Server Error + /microservices/{uuid}/config: + get: + tags: + - Microservices + summary: Gets a microservice config + operationId: getMicroserviceConfig + parameters: + - in: path + name: uuid + description: Microservice Uuid + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/microservicesConfig" + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + patch: + tags: + - Microservices + summary: Updates a microservice config + operationId: updateMicroserviceConfig + parameters: + - in: path + name: uuid + description: Microservice Uuid + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/microservicesConfig" + description: information about microservice config + required: true + responses: + "204": + description: Updated + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + delete: + tags: + - Microservices + summary: Deletes a microservice config + operationId: deleteMicroserviceConfig + parameters: + - in: path + name: uuid + description: Microservice Uuid + required: true + schema: + type: string + security: + - authToken: [] + responses: + "204": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + /microservices/system/{uuid}/config: + get: + tags: + - Microservices + summary: Gets a system microservice config + operationId: getSystemMicroserviceConfig + parameters: + - in: path + name: uuid + description: Microservice Uuid + required: true + schema: + type: string + security: + - authToken: [] + responses: + "200": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + content: + application/json: + schema: + $ref: "#/components/schemas/microservicesConfig" + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + patch: + tags: + - Microservices + summary: Updates a system microservice config + operationId: updateSystemMicroserviceConfig + parameters: + - in: path + name: uuid + description: Microservice Uuid + required: true + schema: + type: string + security: + - authToken: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/microservicesConfig" + description: information about microservice config + required: true + responses: + "204": + description: Updated + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "400": + description: Bad Request + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error + delete: + tags: + - Microservices + summary: Deletes a system microservice config + operationId: deleteSystemMicroserviceConfig + parameters: + - in: path + name: uuid + description: Microservice Uuid + required: true + schema: + type: string + security: + - authToken: [] + responses: + "204": + description: Success + headers: + X-Timestamp: + description: FogController server timestamp + schema: + type: number + "401": + description: Not Authorized + "404": + description: Not Found + "500": + description: Internal Server Error "/microservices/{uuid}/port-mapping": post: tags: @@ -5897,6 +6093,8 @@ components: portExternal: type: string example: 80 + microservicesConfig: + type: string RegistriesListResponse: type: object properties: diff --git a/package-lock.json b/package-lock.json index e2141bf4..3f9900a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@datasance/iofogcontroller", - "version": "3.5.0", + "version": "3.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@datasance/iofogcontroller", - "version": "3.5.0", + "version": "3.5.3", "hasInstallScript": true, "license": "EPL-2.0", "dependencies": { - "@datasance/ecn-viewer": "1.0.0", + "@datasance/ecn-viewer": "1.1.0", "@kubernetes/client-node": "^0.22.3", "@msgpack/msgpack": "^3.1.2", "@opentelemetry/api": "^1.9.0", @@ -19,7 +19,7 @@ "@opentelemetry/instrumentation-http": "^0.200.0", "@opentelemetry/resources": "^1.8.0", "@opentelemetry/sdk-node": "^0.200.0", - "axios": "1.8.4", + "axios": "1.11.0", "bignumber.js": "^9.3.0", "body-parser": "^1.20.3", "child_process": "1.0.2", @@ -426,9 +426,9 @@ } }, "node_modules/@datasance/ecn-viewer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@datasance/ecn-viewer/-/ecn-viewer-1.0.0.tgz", - "integrity": "sha512-dgKXX2wmWQQl3UKt1cUmDXz721Rvl8jEDHJIGi99wPmk98aeF47XWO5oev83FFmHCQiBaIDtO2QXdtWuHaRSxg==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@datasance/ecn-viewer/-/ecn-viewer-1.1.0.tgz", + "integrity": "sha512-M6fcBo7hcx9ooyt//n5bVQMihCBBgn4xyCW4+2OtH+spKw2vu1tFUZWpSCe4C680jYOnECHB4ZkrI84p96wl7w==" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", @@ -2894,12 +2894,12 @@ "dev": true }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -6113,9 +6113,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", diff --git a/package.json b/package.json index 29032157..ac6cad5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@datasance/iofogcontroller", - "version": "3.5.0", + "version": "3.5.3", "description": "ioFog Controller project for Datasance PoT @ datasance.com \\nCopyright (c) 2023 Datasance Teknoloji A.S.", "main": "./src/main.js", "author": "Emirhan Durmus", @@ -55,7 +55,7 @@ "iofog-controller": "src/main.js" }, "dependencies": { - "@datasance/ecn-viewer": "1.0.0", + "@datasance/ecn-viewer": "1.1.0", "@kubernetes/client-node": "^0.22.3", "@msgpack/msgpack": "^3.1.2", "@opentelemetry/api": "^1.9.0", @@ -64,7 +64,7 @@ "@opentelemetry/instrumentation-http": "^0.200.0", "@opentelemetry/resources": "^1.8.0", "@opentelemetry/sdk-node": "^0.200.0", - "axios": "1.8.4", + "axios": "1.11.0", "bignumber.js": "^9.3.0", "body-parser": "^1.20.3", "child_process": "1.0.2", diff --git a/src/controllers/microservices-controller.js b/src/controllers/microservices-controller.js index c599fbb5..302a0fc8 100644 --- a/src/controllers/microservices-controller.js +++ b/src/controllers/microservices-controller.js @@ -85,6 +85,38 @@ const updateSystemMicroserviceYAMLEndPoint = async function (req) { return MicroservicesService.updateSystemMicroserviceEndPoint(microserviceUuid, microservice, false) } +const updateMicroserviceConfigEndPoint = async function (req) { + const microserviceUuid = req.params.uuid + const config = req.body + return MicroservicesService.updateMicroserviceConfigEndPoint(microserviceUuid, config, false) +} + +const getMicroserviceConfigEndPoint = async function (req) { + const microserviceUuid = req.params.uuid + return MicroservicesService.getMicroserviceConfigEndPoint(microserviceUuid, false) +} + +const deleteMicroserviceConfigEndPoint = async function (req) { + const microserviceUuid = req.params.uuid + return MicroservicesService.deleteMicroserviceConfigEndPoint(microserviceUuid, false) +} + +const updateSystemMicroserviceConfigEndPoint = async function (req) { + const microserviceUuid = req.params.uuid + const config = req.body + return MicroservicesService.updateSystemMicroserviceConfigEndPoint(microserviceUuid, config, false) +} + +const getSystemMicroserviceConfigEndPoint = async function (req) { + const microserviceUuid = req.params.uuid + return MicroservicesService.getSystemMicroserviceConfigEndPoint(microserviceUuid, false) +} + +const deleteSystemMicroserviceConfigEndPoint = async function (req) { + const microserviceUuid = req.params.uuid + return MicroservicesService.deleteSystemMicroserviceConfigEndPoint(microserviceUuid, false) +} + const deleteMicroserviceEndPoint = async function (req) { const microserviceUuid = req.params.uuid const microserviceData = req.body || {} @@ -237,6 +269,12 @@ module.exports = { createMicroserviceYAMLEndPoint: (createMicroserviceYAMLEndPoint), updateMicroserviceYAMLEndPoint: (updateMicroserviceYAMLEndPoint), updateSystemMicroserviceYAMLEndPoint: (updateSystemMicroserviceYAMLEndPoint), + updateMicroserviceConfigEndPoint: (updateMicroserviceConfigEndPoint), + getMicroserviceConfigEndPoint: (getMicroserviceConfigEndPoint), + updateSystemMicroserviceConfigEndPoint: (updateSystemMicroserviceConfigEndPoint), + getSystemMicroserviceConfigEndPoint: (getSystemMicroserviceConfigEndPoint), + deleteMicroserviceConfigEndPoint: (deleteMicroserviceConfigEndPoint), + deleteSystemMicroserviceConfigEndPoint: (deleteSystemMicroserviceConfigEndPoint), createMicroserviceExecEndPoint: (createMicroserviceExecEndPoint), deleteMicroserviceExecEndPoint: (deleteMicroserviceExecEndPoint), createSystemMicroserviceExecEndPoint: (createSystemMicroserviceExecEndPoint), diff --git a/src/routes/config.js b/src/routes/config.js index 366d42f2..60d6b347 100644 --- a/src/routes/config.js +++ b/src/routes/config.js @@ -96,7 +96,7 @@ module.exports = [ ] // Add keycloak.protect() middleware to protect the route - await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + await keycloak.protect(['SRE'])(req, res, async () => { const upsertConfigElementEndpoint = ResponseDecorator.handleErrors(ConfigController.upsertConfigElementEndpoint, successCode, errorCodes) const responseObject = await upsertConfigElementEndpoint(req) const user = req.kauth.grant.access_token.content.preferred_username diff --git a/src/routes/microservices.js b/src/routes/microservices.js index b0b08a5f..38c3d083 100644 --- a/src/routes/microservices.js +++ b/src/routes/microservices.js @@ -478,6 +478,216 @@ module.exports = [ }) } }, + { + method: 'get', + path: '/api/v3/microservices/:uuid/config', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + const getMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getMicroserviceConfigEndPoint, + successCode, errorCodes) + const responseObject = await getMicroserviceConfigEndPoint(req) + const user = req.kauth.grant.access_token.content.preferred_username + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/microservices/:uuid/config', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + const updateMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateMicroserviceConfigEndPoint, + successCode, errorCodes) + const responseObject = await updateMicroserviceConfigEndPoint(req) + const user = req.kauth.grant.access_token.content.preferred_username + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'get', + path: '/api/v3/microservices/system/:uuid/config', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_SUCCESS + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await keycloak.protect(['SRE', 'Developer', 'Viewer'])(req, res, async () => { + const getSystemMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.getSystemMicroserviceConfigEndPoint, + successCode, errorCodes) + const responseObject = await getSystemMicroserviceConfigEndPoint(req) + const user = req.kauth.grant.access_token.content.preferred_username + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'patch', + path: '/api/v3/microservices/system/:uuid/config', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await keycloak.protect(['SRE'])(req, res, async () => { + const updateSystemMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.updateSystemMicroserviceConfigEndPoint, + successCode, errorCodes) + const responseObject = await updateSystemMicroserviceConfigEndPoint(req) + const user = req.kauth.grant.access_token.content.preferred_username + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'delete', + path: '/api/v3/microservices/:uuid/config', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await keycloak.protect(['SRE', 'Developer'])(req, res, async () => { + const deleteMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.deleteMicroserviceConfigEndPoint, + successCode, errorCodes) + const responseObject = await deleteMicroserviceConfigEndPoint(req) + const user = req.kauth.grant.access_token.content.preferred_username + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, + { + method: 'delete', + path: '/api/v3/microservices/system/:uuid/config', + middleware: async (req, res) => { + logger.apiReq(req) + + const successCode = constants.HTTP_CODE_NO_CONTENT + const errorCodes = [ + { + code: constants.HTTP_CODE_BAD_REQUEST, + errors: [Errors.ValidationError] + }, + { + code: constants.HTTP_CODE_UNAUTHORIZED, + errors: [Errors.AuthenticationError] + }, + { + code: constants.HTTP_CODE_NOT_FOUND, + errors: [Errors.NotFoundError] + } + ] + + await keycloak.protect(['SRE'])(req, res, async () => { + const deleteSystemMicroserviceConfigEndPoint = ResponseDecorator.handleErrors(MicroservicesController.deleteSystemMicroserviceConfigEndPoint, + successCode, errorCodes) + const responseObject = await deleteSystemMicroserviceConfigEndPoint(req) + const user = req.kauth.grant.access_token.content.preferred_username + res + .status(responseObject.code) + .send(responseObject.body) + + logger.apiRes({ req: req, user: user, res: res, responseObject: responseObject }) + }) + } + }, { method: 'delete', path: '/api/v3/microservices/:uuid', diff --git a/src/services/microservices-service.js b/src/services/microservices-service.js index 173d7bb2..539ef74f 100644 --- a/src/services/microservices-service.js +++ b/src/services/microservices-service.js @@ -1018,6 +1018,145 @@ async function updateMicroserviceEndPoint (microserviceUuid, microserviceData, i } } +async function updateMicroserviceConfigEndPoint (microserviceUuid, config, isCLI, transaction) { + const query = isCLI + ? { + uuid: microserviceUuid + } + : { + uuid: microserviceUuid + } + const microservice = await MicroserviceManager.findOneWithCategory(query, transaction) + if (!microservice) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_MICROSERVICE_UUID, microserviceUuid)) + } + if (microservice.catalogItem && microservice.catalogItem.category === 'SYSTEM') { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SYSTEM_MICROSERVICE_UPDATE, microserviceUuid)) + } + const microserviceConfig = _validateMicroserviceConfig(JSON.stringify(config)) + await MicroserviceManager.update(query, { config: microserviceConfig }, transaction) + const iofogUuid = microservice.iofogUuid + await ChangeTrackingService.update(iofogUuid, ChangeTrackingService.events.microserviceConfig, transaction) + return { + uuid: microserviceUuid + } +} + +async function getMicroserviceConfigEndPoint (microserviceUuid, isCLI, transaction) { + const query = isCLI + ? { + uuid: microserviceUuid + } + : { + uuid: microserviceUuid + } + const microservice = await MicroserviceManager.findOneWithCategory(query, transaction) + if (!microservice) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_MICROSERVICE_UUID, microserviceUuid)) + } + + if (microservice.catalogItem && microservice.catalogItem.category === 'SYSTEM') { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SYSTEM_MICROSERVICE_UPDATE, microserviceUuid)) + } + const microserviceConfig = JSON.parse(microservice.config) + return { + config: microserviceConfig + } +} + +async function deleteMicroserviceConfigEndPoint (microserviceUuid, isCLI, transaction) { + const query = isCLI + ? { + uuid: microserviceUuid + } + : { + uuid: microserviceUuid + } + const microservice = await MicroserviceManager.findOneWithCategory(query, transaction) + if (!microservice) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_MICROSERVICE_UUID, microserviceUuid)) + } + if (microservice.catalogItem && microservice.catalogItem.category === 'SYSTEM') { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SYSTEM_MICROSERVICE_UPDATE, microserviceUuid)) + } + const microserviceConfig = {} + await MicroserviceManager.update(query, { config: JSON.stringify(microserviceConfig) }, transaction) + const iofogUuid = microservice.iofogUuid + await ChangeTrackingService.update(iofogUuid, ChangeTrackingService.events.microserviceConfig, transaction) + return { + uuid: microserviceUuid + } +} + +async function getSystemMicroserviceConfigEndPoint (microserviceUuid, isCLI, transaction) { + const query = isCLI + ? { + uuid: microserviceUuid + } + : { + uuid: microserviceUuid + } + const microservice = await MicroserviceManager.findOneWithCategory(query, transaction) + if (!microservice) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_MICROSERVICE_UUID, microserviceUuid)) + } + if (!microservice.catalogItem || microservice.catalogItem.category !== 'SYSTEM') { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SYSTEM_MICROSERVICE_UPDATE, microserviceUuid)) + } + const microserviceConfig = JSON.parse(microservice.config) + return { + config: microserviceConfig + } +} + +async function updateSystemMicroserviceConfigEndPoint (microserviceUuid, config, isCLI, transaction) { + const query = isCLI + ? { + uuid: microserviceUuid + } + : { + uuid: microserviceUuid + } + const microservice = await MicroserviceManager.findOneWithCategory(query, transaction) + if (!microservice) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_MICROSERVICE_UUID, microserviceUuid)) + } + if (!microservice.catalogItem || microservice.catalogItem.category !== 'SYSTEM') { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SYSTEM_MICROSERVICE_UPDATE, microserviceUuid)) + } + const microserviceConfig = _validateMicroserviceConfig(JSON.stringify(config)) + await MicroserviceManager.update(query, { config: microserviceConfig }, transaction) + const iofogUuid = microservice.iofogUuid + await ChangeTrackingService.update(iofogUuid, ChangeTrackingService.events.microserviceConfig, transaction) + return { + uuid: microserviceUuid + } +} + +async function deleteSystemMicroserviceConfigEndPoint (microserviceUuid, isCLI, transaction) { + const query = isCLI + ? { + uuid: microserviceUuid + } + : { + uuid: microserviceUuid + } + const microservice = await MicroserviceManager.findOneWithCategory(query, transaction) + if (!microservice) { + throw new Errors.NotFoundError(AppHelper.formatMessage(ErrorMessages.INVALID_MICROSERVICE_UUID, microserviceUuid)) + } + if (!microservice.catalogItem || microservice.catalogItem.category !== 'SYSTEM') { + throw new Errors.ValidationError(AppHelper.formatMessage(ErrorMessages.SYSTEM_MICROSERVICE_UPDATE, microserviceUuid)) + } + const microserviceConfig = {} + await MicroserviceManager.update(query, { config: JSON.stringify(microserviceConfig) }, transaction) + const iofogUuid = microservice.iofogUuid + await ChangeTrackingService.update(iofogUuid, ChangeTrackingService.events.microserviceConfig, transaction) + return { + uuid: microserviceUuid + } +} + async function rebuildMicroserviceEndPoint (microserviceUuid, isCLI, transaction) { const query = isCLI ? { @@ -2226,6 +2365,12 @@ module.exports = { listVolumeMappingsEndPoint: TransactionDecorator.generateTransaction(listVolumeMappingsEndPoint), updateMicroserviceEndPoint: TransactionDecorator.generateTransaction(updateMicroserviceEndPoint), updateSystemMicroserviceEndPoint: TransactionDecorator.generateTransaction(updateSystemMicroserviceEndPoint), + updateMicroserviceConfigEndPoint: TransactionDecorator.generateTransaction(updateMicroserviceConfigEndPoint), + getMicroserviceConfigEndPoint: TransactionDecorator.generateTransaction(getMicroserviceConfigEndPoint), + getSystemMicroserviceConfigEndPoint: TransactionDecorator.generateTransaction(getSystemMicroserviceConfigEndPoint), + deleteMicroserviceConfigEndPoint: TransactionDecorator.generateTransaction(deleteMicroserviceConfigEndPoint), + updateSystemMicroserviceConfigEndPoint: TransactionDecorator.generateTransaction(updateSystemMicroserviceConfigEndPoint), + deleteSystemMicroserviceConfigEndPoint: TransactionDecorator.generateTransaction(deleteSystemMicroserviceConfigEndPoint), rebuildMicroserviceEndPoint: TransactionDecorator.generateTransaction(rebuildMicroserviceEndPoint), rebuildSystemMicroserviceEndPoint: TransactionDecorator.generateTransaction(rebuildSystemMicroserviceEndPoint), buildGetMicroserviceResponse: _buildGetMicroserviceResponse, diff --git a/src/websocket/server.js b/src/websocket/server.js index 112e3aa2..57aea7ff 100644 --- a/src/websocket/server.js +++ b/src/websocket/server.js @@ -463,17 +463,44 @@ class WebSocketServer { } } + async getPendingAgentExecIdsFromDB (microserviceUuid, transaction) { + try { + const pendingExecStatus = await MicroserviceExecStatusManager.findAllExcludeFields( + { + microserviceUuid: microserviceUuid, + status: microserviceExecState.PENDING + }, + transaction + ) + + const execIds = pendingExecStatus.map(status => status.execSessionId) + logger.debug('Database query for pending agents:' + JSON.stringify({ + microserviceUuid, + foundExecIds: execIds, + count: execIds.length + })) + + return execIds + } catch (error) { + logger.error('Failed to query database for pending agents:' + JSON.stringify({ + error: error.message, + microserviceUuid + })) + return [] + } + } + async handleUserConnection (ws, req, token, microserviceUuid, transaction) { try { - const { execSessionId } = await this.validateUserConnection(token, microserviceUuid, transaction) - logger.info('User connection: available execSessionId:' + execSessionId) + await this.validateUserConnection(token, microserviceUuid, transaction) + logger.info('User connection validated successfully for microservice:' + microserviceUuid) // Check if there's already an active session for this microservice const existingSession = Array.from(this.sessionManager.sessions.values()) .find(session => session.microserviceUuid === microserviceUuid && session.user && session.user.readyState === WebSocket.OPEN) if (existingSession) { - logger.error('Microservice has already active exec session:' + JSON.stringify({ + logger.debug('Microservice has already active exec session:' + JSON.stringify({ microserviceUuid, existingExecId: existingSession.execId })) @@ -481,61 +508,210 @@ class WebSocketServer { return } - // Get all active execIds - const activeExecIds = Array.from(this.sessionManager.sessions.keys()) - logger.info('Currently active execIds:' + JSON.stringify(activeExecIds)) + // Get pending agent execIds from database (multi-replica compatible) + const pendingAgentExecIds = await this.getPendingAgentExecIdsFromDB(microserviceUuid, transaction) + logger.info('Pending agent execIds from database:' + JSON.stringify(pendingAgentExecIds)) + + // Simplified logic: find any available pending agent + const hasPendingAgents = pendingAgentExecIds.length > 0 + + if (hasPendingAgents) { + // Find any available pending agent + const availableExecId = pendingAgentExecIds[0] + const pendingAgent = this.sessionManager.findPendingAgentForExecId(microserviceUuid, availableExecId) + + if (pendingAgent) { + // Activate session using agent's execId + const session = this.sessionManager.tryActivateSession(microserviceUuid, availableExecId, ws, false, transaction) + if (session) { + logger.info('Session activated for user:', { + execId: availableExecId, + microserviceUuid, + userState: ws.readyState, + agentState: pendingAgent.readyState + }) + this.setupMessageForwarding(availableExecId, transaction) + return + } + } + } - // Get pending agent execIds - const pendingAgentExecIds = this.sessionManager.getPendingAgentExecIds(microserviceUuid) - logger.info('Pending agent execIds:' + JSON.stringify(pendingAgentExecIds)) + // If we reach here, either no pending agent or activation failed + // Add user to pending list to wait for agent + logger.info('No immediate agent available, adding user to pending list:' + JSON.stringify({ + microserviceUuid, + hasPendingAgents, + pendingAgentCount: pendingAgentExecIds.length + })) + this.sessionManager.addPendingUser(microserviceUuid, ws) + + // IMMEDIATE RE-CHECK: Look for any newly available agents after adding user (database query) + const retryPendingAgents = await this.getPendingAgentExecIdsFromDB(microserviceUuid, transaction) + if (retryPendingAgents.length > 0) { + logger.info('Found available agent after adding user, attempting immediate activation:' + JSON.stringify({ + microserviceUuid, + availableExecIds: retryPendingAgents + })) - // Find an available execId that is both not active AND has a pending agent - const availableExecId = execSessionId && !activeExecIds.includes(execSessionId) && pendingAgentExecIds.includes(execSessionId) - ? execSessionId - : null + // Try to activate session with first available agent + const availableExecId = retryPendingAgents[0] + const pendingAgent = this.sessionManager.findPendingAgentForExecId(microserviceUuid, availableExecId) + + if (pendingAgent) { + const session = this.sessionManager.tryActivateSession(microserviceUuid, availableExecId, ws, false, transaction) + if (session) { + logger.info('Session activated immediately after re-check:' + JSON.stringify({ + execId: availableExecId, + microserviceUuid, + userState: ws.readyState, + agentState: pendingAgent.readyState + })) - if (!availableExecId) { - logger.error('No available exec session for user') - ws.close(1008, 'No available exec session for this microservice.') - return + this.setupMessageForwarding(availableExecId, transaction) + return // Exit early, session activated successfully + } + } } - logger.info('User assigned execId:' + availableExecId) - // Check if there's a pending agent with this execId - const pendingAgent = this.sessionManager.findPendingAgentForExecId(microserviceUuid, availableExecId) - if (pendingAgent) { - logger.info('Found pending agent for execId:' + JSON.stringify({ - execId: availableExecId, + // Only proceed with timeout mechanism if we still couldn't activate + logger.info('No immediate agent available after re-check, proceeding with timeout mechanism') + + // Send status message to user when added to pending using STDERR + try { + const statusMsg = { + type: MESSAGE_TYPES.STDERR, + data: Buffer.from('Waiting for agent connection. Please ensure the microservice agent is running.\n'), + microserviceUuid: microserviceUuid, + execId: 'pending', // Since we don't have execSessionId anymore + timestamp: Date.now() + } + const encoded = this.encodeMessage(statusMsg) + ws.send(encoded, { + binary: true, + compress: false, + mask: false, + fin: true + }) + logger.info('Sent waiting status message to user:' + JSON.stringify({ microserviceUuid, - agentState: pendingAgent.readyState + messageType: 'STDERR', + encodedLength: encoded.length })) - // Try to activate session with the selected execId - const session = this.sessionManager.tryActivateSession(microserviceUuid, availableExecId, ws, false, transaction) - if (session) { - logger.info('Session activated for user:', { - execId: availableExecId, + } catch (error) { + logger.warn('Failed to send status message to user:' + JSON.stringify({ + error: error.message, + microserviceUuid + })) + } + + // Start periodic retry timer for pending users (every 10 seconds) + const RETRY_INTERVAL = 10000 + const startTime = Date.now() + const retryTimer = setInterval(async () => { + if (this.sessionManager.isUserStillPending(microserviceUuid, ws)) { + logger.debug('Periodic retry: checking for available agents:' + JSON.stringify({ microserviceUuid, - userState: ws.readyState, - agentState: pendingAgent.readyState - }) - this.setupMessageForwarding(availableExecId, transaction) + retryCount: Math.floor((Date.now() - startTime) / RETRY_INTERVAL) + })) + + try { + const periodicRetryExecIds = await this.getPendingAgentExecIdsFromDB(microserviceUuid, transaction) + if (periodicRetryExecIds.length > 0) { + logger.info('Periodic retry found available agent:' + JSON.stringify({ + microserviceUuid, + availableExecIds: periodicRetryExecIds + })) + + // Attempt session activation with first available agent + const availableExecId = periodicRetryExecIds[0] + const pendingAgent = this.sessionManager.findPendingAgentForExecId(microserviceUuid, availableExecId) + + if (pendingAgent) { + const session = this.sessionManager.tryActivateSession(microserviceUuid, availableExecId, ws, false, transaction) + if (session) { + logger.info('Session activated via periodic retry:' + JSON.stringify({ + execId: availableExecId, + microserviceUuid, + userState: ws.readyState, + agentState: pendingAgent.readyState + })) + + this.setupMessageForwarding(availableExecId, transaction) + clearInterval(retryTimer) // Stop retry timer + return // Exit early, session activated successfully + } + } + } + } catch (retryError) { + logger.warn('Periodic retry failed:' + JSON.stringify({ + error: retryError.message, + microserviceUuid + })) + } } else { - logger.info('Failed to activate session with pending agent:' + JSON.stringify({ - execId: availableExecId, + // User no longer pending, clear retry timer + clearInterval(retryTimer) + } + }, RETRY_INTERVAL) + + // Store timer reference for cleanup + this.sessionManager.setUserRetryTimer(microserviceUuid, ws, retryTimer) + + // Add timeout mechanism for pending users (60 seconds) + const PENDING_USER_TIMEOUT = 60000 + setTimeout(() => { + if (this.sessionManager.isUserStillPending(microserviceUuid, ws)) { + logger.warn('Pending user timeout, closing connection:' + JSON.stringify({ microserviceUuid, - userState: ws.readyState, - agentState: pendingAgent.readyState + timeout: PENDING_USER_TIMEOUT })) - this.sessionManager.addPendingUser(microserviceUuid, ws) + + // Send timeout message before closing + try { + const timeoutMsg = { + type: MESSAGE_TYPES.STDERR, + data: Buffer.from('Timeout waiting for agent connection. Please try again.\n'), + microserviceUuid: microserviceUuid, + execId: 'pending', // Since we don't have execSessionId anymore + timestamp: Date.now() + } + const encoded = this.encodeMessage(timeoutMsg) + ws.send(encoded, { + binary: true, + compress: false, + mask: false, + fin: true + }) + logger.info('Sent timeout message to user:' + JSON.stringify({ + microserviceUuid, + messageType: 'STDERR', + encodedLength: encoded.length + })) + } catch (timeoutError) { + logger.warn('Failed to send timeout message to user:' + JSON.stringify({ + error: timeoutError.message, + microserviceUuid + })) + } + + try { + ws.close(1008, 'Timeout waiting for agent connection') + } catch (closeError) { + logger.error('Error closing timed out user connection:' + JSON.stringify({ + error: closeError.message, + microserviceUuid + })) + } + // Clear retry timer before removing user + const retryTimer = this.sessionManager.getUserRetryTimer(microserviceUuid, ws) + if (retryTimer) { + clearInterval(retryTimer) + this.sessionManager.clearUserRetryTimer(microserviceUuid, ws) + } + + this.sessionManager.removePendingUser(microserviceUuid, ws) } - } else { - logger.info('No pending agent found for user, waiting:' + JSON.stringify({ - execId: availableExecId, - microserviceUuid, - userState: ws.readyState - })) - this.sessionManager.addPendingUser(microserviceUuid, ws) - } + }, PENDING_USER_TIMEOUT) ws.on('close', () => { for (const [execId, session] of this.sessionManager.sessions) { @@ -543,6 +719,14 @@ class WebSocketServer { this.cleanupSession(execId, transaction) } } + + // Clear retry timer before removing user + const retryTimer = this.sessionManager.getUserRetryTimer(microserviceUuid, ws) + if (retryTimer) { + clearInterval(retryTimer) + this.sessionManager.clearUserRetryTimer(microserviceUuid, ws) + } + this.sessionManager.removePendingUser(microserviceUuid, ws) logger.info('User WebSocket disconnected:' + JSON.stringify({ microserviceUuid, @@ -923,6 +1107,11 @@ class WebSocketServer { } // For non-system, SRE or Developer is already checked above + // Check if microservice exec is enabled + if (!microservice.execEnabled) { + throw new Errors.ValidationError('Microservice exec is not enabled') + } + const execStatusArr = await MicroserviceExecStatusManager.findAllExcludeFields({ microserviceUuid: microserviceUuid }, transaction) if (!execStatusArr || execStatusArr.length === 0) { throw new Errors.NotFoundError('Microservice exec status not found') @@ -937,7 +1126,7 @@ class WebSocketServer { throw new Errors.ValidationError('Microservice already has an active session') } - return { execSessionId: execStatus.execSessionId } + return { success: true } // Just indicate validation passed } catch (error) { logger.error('User connection validation failed:' + JSON.stringify({ error: error.message, stack: error.stack })) throw error diff --git a/src/websocket/session-manager.js b/src/websocket/session-manager.js index a9a148be..cc92bf14 100644 --- a/src/websocket/session-manager.js +++ b/src/websocket/session-manager.js @@ -15,6 +15,7 @@ class SessionManager { this.sessions = new Map() this.pendingUsers = new Map() // Map> this.pendingAgents = new Map() // Map> + this.userRetryTimers = new Map() // Map> this.config = config this.cleanupInterval = null logger.info('SessionManager initialized with config:' + JSON.stringify({ @@ -66,12 +67,14 @@ class SessionManager { addPendingUser (microserviceUuid, userWs) { if (!this.pendingUsers.has(microserviceUuid)) { - this.pendingUsers.set(microserviceUuid, new Set()) + this.pendingUsers.set(microserviceUuid, new Map()) } - this.pendingUsers.get(microserviceUuid).add(userWs) + const users = this.pendingUsers.get(microserviceUuid) + users.set(userWs, { timestamp: Date.now() }) + logger.info('Added pending user:' + JSON.stringify({ microserviceUuid, - pendingUserCount: this.pendingUsers.get(microserviceUuid).size + pendingUserCount: users.size })) } @@ -119,6 +122,10 @@ class SessionManager { if (users.size === 0) { this.pendingUsers.delete(microserviceUuid) } + + // Clear retry timer when removing user + this.clearUserRetryTimer(microserviceUuid, userWs) + logger.info('Removed pending user:' + JSON.stringify({ microserviceUuid, remainingUsers: users.size @@ -151,9 +158,8 @@ class SessionManager { findPendingUserForExecId (microserviceUuid, execId) { if (this.pendingUsers.has(microserviceUuid)) { const users = this.pendingUsers.get(microserviceUuid) - // Return the first available user since we don't store execId with users - // The execId will be assigned when creating the session - for (const userWs of users) { + // Find any available user (no execId matching needed) + for (const [userWs] of users.entries()) { if (userWs.readyState === WebSocket.OPEN) { return userWs } @@ -193,6 +199,11 @@ class SessionManager { userState: pendingUser.readyState, agentState: newConnection.readyState })) + await MicroserviceExecStatusManager.update( + { microserviceUuid: microserviceUuid }, + { execSessionId: execId, status: microserviceExecState.ACTIVE }, + transaction + ) } else { await this.addPendingAgent(microserviceUuid, execId, newConnection, transaction) logger.info('No pending user found for agent, added to pending list:' + JSON.stringify({ @@ -265,7 +276,11 @@ class SessionManager { for (const [microserviceUuid, users] of this.pendingUsers) { logger.info(JSON.stringify({ microserviceUuid, - count: users.size + count: users.size, + users: Array.from(users.entries()).map(([ws, info]) => ({ + timestamp: new Date(info.timestamp).toISOString(), + readyState: ws.readyState + })) })) } logger.info('Pending agents:') @@ -490,6 +505,40 @@ class SessionManager { } return [] } + + isUserStillPending (microserviceUuid, userWs) { + if (this.pendingUsers.has(microserviceUuid)) { + const users = this.pendingUsers.get(microserviceUuid) + return users.has(userWs) + } + return false + } + + setUserRetryTimer (microserviceUuid, userWs, timer) { + if (!this.userRetryTimers.has(microserviceUuid)) { + this.userRetryTimers.set(microserviceUuid, new Map()) + } + const timers = this.userRetryTimers.get(microserviceUuid) + timers.set(userWs, timer) + } + + getUserRetryTimer (microserviceUuid, userWs) { + if (this.userRetryTimers.has(microserviceUuid)) { + const timers = this.userRetryTimers.get(microserviceUuid) + return timers.get(userWs) + } + return null + } + + clearUserRetryTimer (microserviceUuid, userWs) { + if (this.userRetryTimers.has(microserviceUuid)) { + const timers = this.userRetryTimers.get(microserviceUuid) + timers.delete(userWs) + if (timers.size === 0) { + this.userRetryTimers.delete(microserviceUuid) + } + } + } } module.exports = SessionManager