From 4bf1a887646c667acfb5b0a8a9c963aab96ed83e Mon Sep 17 00:00:00 2001 From: Zhihe Li Date: Fri, 10 Nov 2023 11:32:21 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20Add=20handler=20to=20mak?= =?UTF-8?q?e=20search=20requests=20to=20client=20daemon=20(#1989)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-11691 --- .../addon/handlers/client-daemon-handler.js | 67 +++++++++++++++++++ .../api/app/handlers/client-daemon-handler.js | 6 ++ addons/api/mirage/scenarios/ipc.js | 4 ++ ui/desktop/app/services/store.js | 21 ++++++ ui/desktop/electron-app/src/ipc/handlers.js | 18 +++++ .../src/services/client-daemon-manager.js | 21 +++++- .../tests/acceptance/authentication-test.js | 4 ++ .../tests/acceptance/cluster-url-test.js | 4 ++ .../projects/sessions/index-test.js | 8 +++ .../projects/sessions/session-test.js | 5 ++ .../acceptance/projects/targets/index-test.js | 8 +++ .../projects/targets/target-test.js | 8 +++ ui/desktop/tests/acceptance/scopes-test.js | 39 ++--------- ui/desktop/tests/helpers/window-mock-ipc.js | 4 ++ ui/desktop/tests/unit/services/store-test.js | 12 ++++ 15 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 addons/api/addon/handlers/client-daemon-handler.js create mode 100644 addons/api/app/handlers/client-daemon-handler.js create mode 100644 ui/desktop/app/services/store.js create mode 100644 ui/desktop/tests/unit/services/store-test.js diff --git a/addons/api/addon/handlers/client-daemon-handler.js b/addons/api/addon/handlers/client-daemon-handler.js new file mode 100644 index 000000000..2aec52fe4 --- /dev/null +++ b/addons/api/addon/handlers/client-daemon-handler.js @@ -0,0 +1,67 @@ +import { pluralize } from 'ember-inflector'; + +/** + * Not all types are yet supported by the client daemon so we'll + * just whitelist the ones we need to start with. + * @type {string[]} + */ +const supportedTypes = ['target', 'session']; + +/** + * Handler to sit in front of the API request layer + * so we can request from the daemon first + */ +const ClientDaemonHandler = { + async request(context, next) { + switch (context.request.op) { + case 'query': { + const { store, data } = context.request; + const { type, query } = data; + const isClientDaemonRunning = await this.ipc.invoke( + 'isClientDaemonRunning' + ); + + if (!supportedTypes.includes(type) || !isClientDaemonRunning) { + return next(context.request); + } + + // TODO: Remove usages of recursive from callers and scope_id since + // all calls to daemon are recursive, scope_id can be part of search query instead + // eslint-disable-next-line no-unused-vars + let { recursive, scope_id, ...modifiedQuery } = query; + const auth_token_id = this.session.data?.authenticated?.id; + modifiedQuery = { + ...modifiedQuery, + auth_token_id, + resource: pluralize(type), + }; + + // Currently returns with a singular top level field with resource name + // e.g. { targets: [...] } or { sessions: [..] } + // So this just unwraps to the array, or undefined + const [results] = Object.values( + await this.ipc.invoke('searchClientDaemon', modifiedQuery) + ); + const payload = { items: results ?? [] }; + + const schema = store.modelFor(type); + const serializer = store.serializerFor(type); + const normalizedPayload = serializer.normalizeResponse( + store, + schema, + payload, + null, + 'query' + ); + + // TODO: Add some pagination + + return store.push(normalizedPayload); + } + default: + return next(context.request); + } + }, +}; + +export default ClientDaemonHandler; diff --git a/addons/api/app/handlers/client-daemon-handler.js b/addons/api/app/handlers/client-daemon-handler.js new file mode 100644 index 000000000..07804ca85 --- /dev/null +++ b/addons/api/app/handlers/client-daemon-handler.js @@ -0,0 +1,6 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export { default } from 'api/handlers/client-daemon-handler'; diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js index ee92196f3..3f868724d 100644 --- a/addons/api/mirage/scenarios/ipc.js +++ b/addons/api/mirage/scenarios/ipc.js @@ -183,6 +183,10 @@ export default function initializeMockIPC(server, config) { return faker.system.filePath(); } addTokenToClientDaemon() {} + searchClientDaemon() {} + isClientDaemonRunning() { + return false; + } } /** diff --git a/ui/desktop/app/services/store.js b/ui/desktop/app/services/store.js new file mode 100644 index 000000000..3d59eccb7 --- /dev/null +++ b/ui/desktop/app/services/store.js @@ -0,0 +1,21 @@ +import Store, { CacheHandler } from '@ember-data/store'; +import RequestManager from '@ember-data/request'; +import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; +import { inject as service } from '@ember/service'; +import ClientDaemonHandler from 'api/handlers/client-daemon-handler'; + +export default class extends Store { + @service ipc; + @service session; + + requestManager = new RequestManager(); + + constructor(args) { + super(args); + + ClientDaemonHandler.request = ClientDaemonHandler.request.bind(this); + + this.requestManager.use([ClientDaemonHandler, LegacyNetworkHandler]); + this.requestManager.useCache(CacheHandler); + } +} diff --git a/ui/desktop/electron-app/src/ipc/handlers.js b/ui/desktop/electron-app/src/ipc/handlers.js index 4eaee0c23..175554e10 100644 --- a/ui/desktop/electron-app/src/ipc/handlers.js +++ b/ui/desktop/electron-app/src/ipc/handlers.js @@ -119,6 +119,9 @@ handle('closeWindow', () => app.quit()); */ handle('checkCommand', async (command) => which(command, { nothrow: true })); +/** + * Adds the user's token to the client daemon. + */ handle('addTokenToClientDaemon', async (data) => clientDaemonManager.addToken(data) ); @@ -132,6 +135,21 @@ handle('checkOS', () => ({ isWindows: isWindows(), })); +/** + * Call the client daemon's search endpoint to retrieve cached results. + */ +handle('searchClientDaemon', async (request) => + clientDaemonManager.search(request) +); + +/** + * Check to see if the client daemon is running. We use the presence of a + * socket path as a proxy for whether the daemon is running. + */ +handle('isClientDaemonRunning', async (request) => + Boolean(clientDaemonManager.socketPath) +); + /** * Handler to help create terminal windows. We don't use the helper `handle` method * as we need access to the event and don't need to be using `ipcMain.handle`. diff --git a/ui/desktop/electron-app/src/services/client-daemon-manager.js b/ui/desktop/electron-app/src/services/client-daemon-manager.js index 4d59d6943..f3457e4d9 100644 --- a/ui/desktop/electron-app/src/services/client-daemon-manager.js +++ b/ui/desktop/electron-app/src/services/client-daemon-manager.js @@ -24,7 +24,7 @@ class ClientDaemonManager { const data = spawnSync(daemonStatusCommand); const status = jsonify(data); - this.#socketPath = status.socket_address; + this.#socketPath = status?.socket_address; return status; } @@ -33,7 +33,13 @@ class ClientDaemonManager { * @returns {Promise} */ async start() { - const startDaemonCommand = ['daemon', 'start']; + // TODO: Finalize how often the daemon should be refreshing + const startDaemonCommand = [ + 'daemon', + 'start', + '-refresh-interval-seconds', + '10', + ]; await spawn(startDaemonCommand); this.status(); } @@ -67,6 +73,17 @@ class ClientDaemonManager { }; return unixSocketRequest(request, postBody); } + + search(requestData) { + const queryString = new URLSearchParams(requestData); + + const request = { + method: 'GET', + path: `http://internal.boundary.local/v1/search?${queryString.toString()}`, + socketPath: this.#socketPath, + }; + return unixSocketRequest(request); + } } // Export an instance so we get a singleton diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js index b2968f602..d49d18f18 100644 --- a/ui/desktop/tests/acceptance/authentication-test.js +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -22,6 +22,7 @@ import { invalidateSession, } from 'ember-simple-auth/test-support'; import WindowMockIPC from '../helpers/window-mock-ipc'; +import Store from 'api/services/store'; module('Acceptance | authentication', function (hooks) { setupApplicationTest(hooks); @@ -128,6 +129,9 @@ module('Acceptance | authentication', function (hooks) { // Mock the postMessage interface used by IPC. this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); }); test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { diff --git a/ui/desktop/tests/acceptance/cluster-url-test.js b/ui/desktop/tests/acceptance/cluster-url-test.js index eede43cb0..dfe9c7747 100644 --- a/ui/desktop/tests/acceptance/cluster-url-test.js +++ b/ui/desktop/tests/acceptance/cluster-url-test.js @@ -13,6 +13,7 @@ import { invalidateSession } from 'ember-simple-auth/test-support'; import { setupBrowserFakes } from 'ember-browser-services/test-support'; import WindowMockIPC from '../helpers/window-mock-ipc'; import config from '../../config/environment'; +import Store from 'api/services/store'; module('Acceptance | clusterUrl', function (hooks) { setupApplicationTest(hooks); @@ -100,6 +101,9 @@ module('Acceptance | clusterUrl', function (hooks) { urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`; urls.projects = `${urls.scopes.global}/projects`; urls.targets = `${urls.projects}/targets`; + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); }); hooks.afterEach(function () { diff --git a/ui/desktop/tests/acceptance/projects/sessions/index-test.js b/ui/desktop/tests/acceptance/projects/sessions/index-test.js index 9417c1bcd..1e6f3f0e5 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/index-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/index-test.js @@ -22,6 +22,7 @@ import { STATUS_SESSION_CANCELING, STATUS_SESSION_TERMINATED, } from 'api/models/session'; +import Store from 'api/services/store'; module('Acceptance | projects | sessions', function (hooks) { setupApplicationTest(hooks); @@ -122,6 +123,13 @@ module('Acceptance | projects | sessions', function (hooks) { const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); + }); + + hooks.afterEach(function () { + sinon.restore(); }); test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { diff --git a/ui/desktop/tests/acceptance/projects/sessions/session-test.js b/ui/desktop/tests/acceptance/projects/sessions/session-test.js index a919cd783..e7f9c0e8d 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/session-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/session-test.js @@ -15,6 +15,7 @@ import { authenticateSession } from 'ember-simple-auth/test-support'; import sinon from 'sinon'; import WindowMockIPC from '../../../helpers/window-mock-ipc'; import { STATUS_SESSION_ACTIVE } from 'api/models/session'; +import Store from 'api/services/store'; module('Acceptance | projects | sessions | session', function (hooks) { setupApplicationTest(hooks); @@ -121,9 +122,13 @@ module('Acceptance | projects | sessions | session', function (hooks) { const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); }); hooks.afterEach(function () { + sinon.restore(); // reset onUncaughtException to original state QUnit.onUncaughtException = originalUncaughtException; }); diff --git a/ui/desktop/tests/acceptance/projects/targets/index-test.js b/ui/desktop/tests/acceptance/projects/targets/index-test.js index f777924a3..1d64e499d 100644 --- a/ui/desktop/tests/acceptance/projects/targets/index-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/index-test.js @@ -21,6 +21,7 @@ import { invalidateSession, currentSession, } from 'ember-simple-auth/test-support'; +import Store from 'api/services/store'; module('Acceptance | projects | targets', function (hooks) { setupApplicationTest(hooks); @@ -138,6 +139,13 @@ module('Acceptance | projects | targets', function (hooks) { const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); + }); + + hooks.afterEach(function () { + sinon.restore(); }); test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) { diff --git a/ui/desktop/tests/acceptance/projects/targets/target-test.js b/ui/desktop/tests/acceptance/projects/targets/target-test.js index b4c9321d1..ebdfb26b3 100644 --- a/ui/desktop/tests/acceptance/projects/targets/target-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/target-test.js @@ -10,6 +10,7 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import sinon from 'sinon'; import { authenticateSession } from 'ember-simple-auth/test-support'; import WindowMockIPC from '../../../helpers/window-mock-ipc'; +import Store from 'api/services/store'; module('Acceptance | projects | targets | target', function (hooks) { setupApplicationTest(hooks); @@ -129,6 +130,13 @@ module('Acceptance | projects | targets | target', function (hooks) { const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); + }); + + hooks.afterEach(function () { + sinon.restore(); }); test('user can connect to a target with an address', async function (assert) { diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js index 5bc9e94e2..a459b9341 100644 --- a/ui/desktop/tests/acceptance/scopes-test.js +++ b/ui/desktop/tests/acceptance/scopes-test.js @@ -15,14 +15,13 @@ import { authenticateSession, invalidateSession, } from 'ember-simple-auth/test-support'; +import WindowMockIPC from '../helpers/window-mock-ipc'; +import Store from 'api/services/store'; module('Acceptance | scopes', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); - let mockIPC; - let messageHandler; - const instances = { scopes: { global: null, @@ -129,42 +128,18 @@ module('Acceptance | scopes', function (hooks) { urls.globalTargets = `${urls.globalProjects}/targets`; urls.target = `${urls.targets}/${instances.target.id}`; - class MockIPC { - clusterUrl = null; - - invoke(method, payload) { - return this[method](payload); - } - - getClusterUrl() { - return this.clusterUrl; - } - - setClusterUrl(clusterUrl) { - this.clusterUrl = clusterUrl; - return this.clusterUrl; - } - } - - mockIPC = new MockIPC(); - messageHandler = async function (event) { - if (event.origin !== window.location.origin) return; - const { method, payload } = event.data; - if (method) { - const response = await mockIPC.invoke(method, payload); - event.ports[0].postMessage(response); - } - }; - - window.addEventListener('message', messageHandler); + this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); const ipcService = this.owner.lookup('service:ipc'); stubs.ipcService = sinon.stub(ipcService, 'invoke'); + + // Use the original store so we don't try and hit the client daemon + this.owner.register('service:store', Store); }); hooks.afterEach(function () { - window.removeEventListener('message', messageHandler); + sinon.restore(); }); test('visiting index', async function (assert) { diff --git a/ui/desktop/tests/helpers/window-mock-ipc.js b/ui/desktop/tests/helpers/window-mock-ipc.js index 2fa46a246..d6d888840 100644 --- a/ui/desktop/tests/helpers/window-mock-ipc.js +++ b/ui/desktop/tests/helpers/window-mock-ipc.js @@ -25,6 +25,10 @@ class MockIPC { hasMacOSChrome() {} showWindowActions() {} addTokenToClientDaemon() {} + searchClientDaemon() {} + isClientDaemonRunning() { + return false; + } } /** diff --git a/ui/desktop/tests/unit/services/store-test.js b/ui/desktop/tests/unit/services/store-test.js new file mode 100644 index 000000000..18e6ac995 --- /dev/null +++ b/ui/desktop/tests/unit/services/store-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'desktop/tests/helpers'; + +module('Unit | Service | store', function (hooks) { + setupTest(hooks); + + // TODO: Replace this with your real tests. + test('it exists', function (assert) { + let service = this.owner.lookup('service:store'); + assert.ok(service); + }); +});