diff --git a/.devcontainer/chefs_local/local.json.sample b/.devcontainer/chefs_local/local.json.sample index 676f2edd9..7217a0c0e 100644 --- a/.devcontainer/chefs_local/local.json.sample +++ b/.devcontainer/chefs_local/local.json.sample @@ -61,6 +61,18 @@ "proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7" } }, + "features": { + "eventStreamService": true + }, + "eventStreamService": { + "servers": "localhost:4222,localhost:4223,localhost:4224", + "websockets": "false", + "streamName": "CHEFS", + "source": "chefs-local", + "domain": "forms", + "username": "chefs", + "password": "password" + }, "serviceClient": { "commonServices": { "ches": { diff --git a/.devcontainer/chefs_local/test.json b/.devcontainer/chefs_local/test.json index 5928abdca..b957b8ae6 100644 --- a/.devcontainer/chefs_local/test.json +++ b/.devcontainer/chefs_local/test.json @@ -61,6 +61,17 @@ "proxy": "5fb2054478353fd8d514056d1745b3a9eef066deadda4b90967af7ca65ce6505" } }, + "features": { + "eventStreamService": false + }, + "eventStreamService": { + "servers": "localhost:4222,localhost:4223,localhost:4224", + "streamName": "CHEFS", + "source": "chefs", + "domain": "forms", + "username": "chefs", + "password": "password" + }, "serviceClient": { "commonServices": { "ches": { diff --git a/.github/actions/deploy-to-environment/action.yaml b/.github/actions/deploy-to-environment/action.yaml index e2bdd6ad9..cf72c8751 100644 --- a/.github/actions/deploy-to-environment/action.yaml +++ b/.github/actions/deploy-to-environment/action.yaml @@ -90,6 +90,15 @@ runs: run: | oc process --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f openshift/app.cm.yaml -p NAMESPACE=${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -p APP_NAME=${{ inputs.acronym }} -p JOB_NAME=${{ inputs.job_name }} -p SERVER_HOST=${{ inputs.server_host }} -o yaml | oc apply --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f - + - name: Deploy event stream ConfigMaps + shell: bash + run: | + if [[ "${{ inputs.job_name }}" == pr-* ]]; then + oc process --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f openshift/ess.cm.yaml -p APP_NAME=${{ inputs.acronym }} -p JOB_NAME=${{ inputs.job_name }} -p SOURCE=${{ inputs.job_name }} -o yaml | oc apply --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f - + else + oc process --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f openshift/ess.cm.yaml -p APP_NAME=${{ inputs.acronym }} -p JOB_NAME=${{ inputs.job_name }} --param-file=openshift/ess.${{ inputs.namespace_environment }}.param -o yaml | oc apply --namespace ${{ inputs.namespace_prefix }}-${{ inputs.namespace_environment }} -f - + fi + - name: Deploy App shell: bash run: | diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index ddef99018..b25b4f0aa 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,14 +1,25 @@ - + # Description - - - + -## Types of changes +## Type of Change - + @@ -18,6 +29,7 @@ + @@ -27,8 +39,10 @@ This is a breaking change because ... ## Checklist - - + - [ ] I have read the [CONTRIBUTING](/bcgov/common-hosted-form-service/blob/main/CONTRIBUTING.md) doc - [ ] I have checked that unit tests pass locally with my changes @@ -39,4 +53,8 @@ This is a breaking change because ... ## Further comments - + diff --git a/app/app.js b/app/app.js index 7a787470c..848e500fe 100644 --- a/app/app.js +++ b/app/app.js @@ -11,15 +11,22 @@ const middleware = require('./src/forms/common/middleware'); const v1Router = require('./src/routes/v1'); const DataConnection = require('./src/db/dataConnection'); +const { featureFlags } = require('./src/components/featureFlags'); const dataConnection = new DataConnection(); +const { eventStreamService } = require('./src/components/eventStreamService'); + const apiRouter = express.Router(); const state = { connections: { data: false, + eventStreamService: false, }, ready: false, shutdown: false, }; + +if (!featureFlags.eventStreamService) delete state.connections.eventStreamService; + let probeId; const app = express(); app.use(compression()); @@ -58,7 +65,16 @@ apiRouter.use('/config', (_req, res, next) => { const frontend = config.get('frontend'); // we will need to pass const uploads = config.get('files.uploads'); - const feConfig = { ...frontend, uploads: uploads }; + const features = config.get('features'); + const feConfig = { ...frontend, uploads: uploads, features: features }; + if (featureFlags.eventStreamService) { + let ess = config.util.cloneDeep(config.get('eventStreamService')); + delete ess['username']; + delete ess['password']; + feConfig['eventStreamService'] = { + ...ess, + }; + } res.status(200).json(feConfig); } catch (err) { next(err); @@ -157,6 +173,7 @@ function cleanup() { log.info('Cleaning up...', { function: 'cleanup' }); clearInterval(probeId); + if (featureFlags.eventStreamService) eventStreamService.closeConnection(); dataConnection.close(() => process.exit()); // Wait 10 seconds max before hard exiting @@ -172,6 +189,10 @@ function initializeConnections() { // Initialize connections and exit if unsuccessful const tasks = [dataConnection.checkAll()]; + if (featureFlags.eventStreamService) { + tasks.push(eventStreamService.checkConnection()); + } + Promise.all(tasks) .then((results) => { state.connections.data = results[0]; @@ -180,9 +201,23 @@ function initializeConnections() { log.info('DataConnection Reachable', { function: 'initializeConnections', }); + + if (featureFlags.eventStreamService) { + state.connections.eventStreamService = results[1]; + if (state.connections.eventStreamService) { + log.info('EventStreamService Reachable', { + function: 'initializeConnections', + }); + } + } else { + log.info('EventStreamService feature is not enabled.'); + } }) .catch((error) => { log.error(`Initialization failed: Database OK = ${state.connections.data}`, { function: 'initializeConnections' }); + if (featureFlags.eventStreamService) + log.error(`Initialization failed: EventStreamService OK = ${state.connections.eventStreamService}`, { function: 'initializeConnections' }); + log.error('Connection initialization failure', error.message, { function: 'initializeConnections', }); @@ -198,7 +233,16 @@ function initializeConnections() { function: 'initializeConnections', }); // Start periodic 10 second connection probe check - probeId = setInterval(checkConnections, 10000); + probeId = setInterval(checkConnections, 30000); + } else { + log.error(`Service not ready to accept traffic`, { + function: 'initializeConnections', + }); + log.error(`Database connected = ${state.connections.data}`, { function: 'initializeConnections' }); + if (featureFlags.eventStreamService) log.error(`EventStreamService connected = ${state.connections.eventStreamService}`, { function: 'initializeConnections' }); + + process.exitCode = 1; + shutdown(); } }); } @@ -212,9 +256,12 @@ function checkConnections() { const wasReady = state.ready; if (!state.shutdown) { const tasks = [dataConnection.checkConnection()]; + if (featureFlags.eventStreamService) tasks.push(eventStreamService.checkConnection()); Promise.all(tasks).then((results) => { state.connections.data = results[0]; + if (featureFlags.eventStreamService) state.connections.eventStreamService = results[1]; + state.ready = Object.values(state.connections).every((x) => x); if (!wasReady && state.ready) log.info('Service ready to accept traffic', { @@ -222,6 +269,8 @@ function checkConnections() { }); log.verbose(state); if (!state.ready) { + log.error(`Database connected = ${state.connections.data}`, { function: 'checkConnections' }); + if (featureFlags.eventStreamService) log.error(`EventStreamService connected = ${state.connections.eventStreamService}`, { function: 'checkConnections' }); process.exitCode = 1; shutdown(); } diff --git a/app/config/custom-environment-variables.json b/app/config/custom-environment-variables.json index 8b2ef444f..b86b6c27f 100755 --- a/app/config/custom-environment-variables.json +++ b/app/config/custom-environment-variables.json @@ -58,6 +58,18 @@ "proxy": "SERVER_ENCRYPTION_PROXY" } }, + "features": { + "eventStreamService": "FEATURES_EVENTSTREAMSERVICE" + }, + "eventStreamService": { + "servers": "EVENTSTREAMSERVICE_SERVERS", + "websockets": "EVENTSTREAMSERVICE_WEBSOCKETS", + "streamName": "EVENTSTREAMSERVICE_STREAMNAME", + "source": "EVENTSTREAMSERVICE_SOURCE", + "domain": "EVENTSTREAMSERVICE_DOMAIN", + "username": "EVENTSTREAMSERVICE_USERNAME", + "password": "EVENTSTREAMSERVICE_PASSWORD" + }, "serviceClient": { "commonServices": { "ches": { diff --git a/app/config/default.json b/app/config/default.json index 931c2592b..29429e874 100644 --- a/app/config/default.json +++ b/app/config/default.json @@ -61,6 +61,18 @@ "proxy": "352f7c24819086bf3df5a38c1a40586045f73e0007440c9d27d59ee8560e3fe7" } }, + "features": { + "eventStreamService": true + }, + "eventStreamService": { + "servers": "localhost:4222,localhost:4223,localhost:4224", + "websockets": "false", + "streamName": "CHEFS", + "source": "chefs-local", + "domain": "forms", + "username": "chefs", + "password": "password" + }, "serviceClient": { "commonServices": { "ches": { diff --git a/app/frontend/src/components/admin/FormComponentsProactiveHelp.vue b/app/frontend/src/components/admin/FormComponentsProactiveHelp.vue index 844c89b78..e424f2922 100644 --- a/app/frontend/src/components/admin/FormComponentsProactiveHelp.vue +++ b/app/frontend/src/components/admin/FormComponentsProactiveHelp.vue @@ -1,172 +1,80 @@ - diff --git a/app/frontend/src/components/designer/FormDesigner.vue b/app/frontend/src/components/designer/FormDesigner.vue index 7abbab56c..b10ecb1c1 100644 --- a/app/frontend/src/components/designer/FormDesigner.vue +++ b/app/frontend/src/components/designer/FormDesigner.vue @@ -652,6 +652,7 @@ export default { apiIntegration: this.form.apiIntegration, useCase: this.form.useCase, labels: this.form.labels, + eventStreamConfig: this.form.eventStreamConfig, }); // update user labels with any new added labels if ( diff --git a/app/frontend/src/components/designer/FormSettings.vue b/app/frontend/src/components/designer/FormSettings.vue index 634d3c2e9..12c511a4f 100644 --- a/app/frontend/src/components/designer/FormSettings.vue +++ b/app/frontend/src/components/designer/FormSettings.vue @@ -5,6 +5,9 @@ import FormGeneralSettings from '~/components/designer/settings/FormGeneralSetti import FormFunctionalitySettings from '~/components/designer/settings/FormFunctionalitySettings.vue'; import FormScheduleSettings from '~/components/designer/settings/FormScheduleSettings.vue'; import FormSubmissionSettings from '~/components/designer/settings/FormSubmissionSettings.vue'; +import FormEventStreamSettings from '~/components/designer/settings/FormEventStreamSettings.vue'; + +import { useAppStore } from '~/store/app'; import { useFormStore } from '~/store/form'; export default { @@ -14,6 +17,7 @@ export default { FormFunctionalitySettings, FormScheduleSettings, FormSubmissionSettings, + FormEventStreamSettings, }, props: { disabled: { @@ -24,6 +28,10 @@ export default { computed: { ...mapWritableState(useFormStore, ['form']), ...mapState(useFormStore, ['isFormPublished', 'isRTL']), + eventStreamEnabled() { + const appStore = useAppStore(); + return appStore.config?.features?.eventStreamService; + }, }, }; @@ -46,6 +54,9 @@ export default { + + + diff --git a/app/frontend/src/components/designer/settings/FormEventStreamSettings.vue b/app/frontend/src/components/designer/settings/FormEventStreamSettings.vue new file mode 100644 index 000000000..0ec6b9a54 --- /dev/null +++ b/app/frontend/src/components/designer/settings/FormEventStreamSettings.vue @@ -0,0 +1,274 @@ + + + diff --git a/app/frontend/src/components/forms/ExportSubmissions.vue b/app/frontend/src/components/forms/ExportSubmissions.vue index 870a25cb8..fd0f8b1f3 100644 --- a/app/frontend/src/components/forms/ExportSubmissions.vue +++ b/app/frontend/src/components/forms/ExportSubmissions.vue @@ -1,7 +1,8 @@ -